オブジェクトとプリミティブの根本的な違いの1つは、オブジェクトは「参照によって」保存およびコピーされるのに対し、プリミティブ値(文字列、数値、ブール値など)は常に「値全体として」コピーされることです。
値のコピー時に何が起こるかを少し詳しく見てみると、簡単に理解できます。
文字列などのプリミティブから始めましょう。
ここでは、message
のコピーをphrase
に入れています。
let message = "Hello!";
let phrase = message;
その結果、それぞれが文字列"Hello!"
を格納する、2つの独立した変数ができました。
かなり分かりやすい結果ですよね?
オブジェクトはそうではありません。
オブジェクトに割り当てられた変数は、オブジェクト自体ではなく、「メモリアドレス」、つまりオブジェクトへの「参照」を格納します。
このような変数の例を見てみましょう。
let user = {
name: "John"
};
そして、これがメモリに実際に保存される方法です。
オブジェクトはメモリのどこか(図の右側)に保存され、user
変数(左側)はその参照を持っています。
user
のようなオブジェクト変数は、オブジェクトのアドレスが書かれた紙切れのようなものだと考えることができます。
オブジェクトで操作を行う場合(例:プロパティuser.name
を取得する場合)、JavaScriptエンジンはそのアドレスに何が存在するかを確認し、実際のオブジェクトに対して操作を実行します。
これが重要な理由です。
オブジェクト変数をコピーすると、参照はコピーされますが、オブジェクト自体は複製されません。
例えば
let user = { name: "John" };
let admin = user; // copy the reference
これで、それぞれ同じオブジェクトを参照する2つの変数ができました。
ご覧のように、オブジェクトは1つしかありませんが、参照する変数は2つあります。
どちらの変数を使用してでもオブジェクトにアクセスし、その内容を変更できます。
let user = { name: 'John' };
let admin = user;
admin.name = 'Pete'; // changed by the "admin" reference
alert(user.name); // 'Pete', changes are seen from the "user" reference
これは、2つの鍵のあるキャビネットがあり、そのうちの1つ(admin
)を使ってキャビネットに入り、変更を加えたようなものです。その後、別の鍵(user
)を使用しても、同じキャビネットを開けて、変更された内容にアクセスできます。
参照による比較
2つのオブジェクトは、同じオブジェクトである場合にのみ等しくなります。
例えば、ここではa
とb
は同じオブジェクトを参照しているので、等しくなります。
let a = {};
let b = a; // copy the reference
alert( a == b ); // true, both variables reference the same object
alert( a === b ); // true
そしてここでは、2つの独立したオブジェクトは、たとえ似ていても(どちらも空ですが)、等しくありません。
let a = {};
let b = {}; // two independent objects
alert( a == b ); // false
obj1 > obj2
のような比較や、プリミティブに対する比較obj == 5
の場合、オブジェクトはプリミティブに変換されます。オブジェクトの変換方法についてはすぐに学習しますが、正直なところ、このような比較はめったに必要ありません。通常、プログラミングミスによって発生します。
オブジェクトを参照として格納することの重要な副作用は、const
として宣言されたオブジェクトは変更できるということです。
例えば
const user = {
name: "John"
};
user.name = "Pete"; // (*)
alert(user.name); // Pete
(*)
行がエラーを引き起こすと思われるかもしれませんが、そうではありません。user
の値は一定であり、常に同じオブジェクトを参照する必要がありますが、そのオブジェクトのプロパティは自由に変更できます。
つまり、const user
は、全体としてuser=...
を設定しようとするときにのみエラーが発生します。
つまり、オブジェクトのプロパティを本当に定数にする必要がある場合も、全く異なる方法を使用できます。それはプロパティフラグと記述子の章で説明します。
クローンとマージ、Object.assign
したがって、オブジェクト変数をコピーすると、同じオブジェクトへの参照がもう1つ作成されます。
しかし、オブジェクトを複製する必要がある場合はどうでしょうか?
新しいオブジェクトを作成し、そのプロパティを反復処理してプリミティブレベルでコピーすることにより、既存のオブジェクトの構造を複製できます。
このように
let user = {
name: "John",
age: 30
};
let clone = {}; // the new empty object
// let's copy all user properties into it
for (let key in user) {
clone[key] = user[key];
}
// now clone is a fully independent object with the same content
clone.name = "Pete"; // changed the data in it
alert( user.name ); // still John in the original object
Object.assignメソッドを使用することもできます。
構文は次のとおりです。
Object.assign(dest, ...sources)
- 最初の引数
dest
はターゲットオブジェクトです。 - それ以降の引数は、ソースオブジェクトのリストです。
すべてのソースオブジェクトのプロパティをターゲットdest
にコピーし、結果として返します。
たとえば、user
オブジェクトがあり、いくつかの権限を追加してみましょう。
let user = { name: "John" };
let permissions1 = { canView: true };
let permissions2 = { canEdit: true };
// copies all properties from permissions1 and permissions2 into user
Object.assign(user, permissions1, permissions2);
// now user = { name: "John", canView: true, canEdit: true }
alert(user.name); // John
alert(user.canView); // true
alert(user.canEdit); // true
コピーされたプロパティ名が既に存在する場合は、上書きされます。
let user = { name: "John" };
Object.assign(user, { name: "Pete" });
alert(user.name); // now user = { name: "Pete" }
Object.assign
を使用して、単純なオブジェクトのクローンを作成することもできます。
let user = {
name: "John",
age: 30
};
let clone = Object.assign({}, user);
alert(clone.name); // John
alert(clone.age); // 30
ここでは、user
のすべてのプロパティを空のオブジェクトにコピーして返します。
オブジェクトのクローン作成には、スプレッド構文clone = {...user}
を使用するなど、他の方法もあります。これはチュートリアルの後半で説明します。
ネストされたクローン
これまでのところ、user
のすべてのプロパティはプリミティブであると仮定していました。しかし、プロパティは他のオブジェクトへの参照である可能性があります。
このように
let user = {
name: "John",
sizes: {
height: 182,
width: 50
}
};
alert( user.sizes.height ); // 182
ここでclone.sizes = user.sizes
をコピーするだけでは不十分です。なぜなら、user.sizes
はオブジェクトであり、参照によってコピーされるため、clone
とuser
は同じサイズを共有することになるからです。
let user = {
name: "John",
sizes: {
height: 182,
width: 50
}
};
let clone = Object.assign({}, user);
alert( user.sizes === clone.sizes ); // true, same object
// user and clone share sizes
user.sizes.width = 60; // change a property from one place
alert(clone.sizes.width); // 60, get the result from the other one
これを修正し、user
とclone
を真に別々のオブジェクトにするには、user[key]
の各値を検査し、オブジェクトの場合はその構造も複製するクローンループを使用する必要があります。これは「ディープクローン」または「構造化クローン」と呼ばれます。structuredCloneメソッドは、ディープクローンを実装しています。
structuredClone
structuredClone(object)
呼び出しは、すべてのネストされたプロパティを持つobject
をクローンします。
これが私たちの例での使用方法です。
let user = {
name: "John",
sizes: {
height: 182,
width: 50
}
};
let clone = structuredClone(user);
alert( user.sizes === clone.sizes ); // false, different objects
// user and clone are totally unrelated now
user.sizes.width = 60; // change a property from one place
alert(clone.sizes.width); // 50, not related
structuredClone
メソッドは、オブジェクト、配列、プリミティブ値など、ほとんどのデータ型をクローンできます。
オブジェクトのプロパティがオブジェクト自体を参照する場合(直接的または参照のチェーンを介して)、循環参照もサポートします。
例えば
let user = {};
// let's create a circular reference:
// user.me references the user itself
user.me = user;
let clone = structuredClone(user);
alert(clone.me === clone); // true
ご覧のように、clone.me
はuser
ではなくclone
を参照しています!したがって、循環参照も正しくクローンされました。
ただし、structuredClone
が失敗する場合もあります。
たとえば、オブジェクトに関数プロパティがある場合
// error
structuredClone({
f: function() {}
});
関数プロパティはサポートされていません。
このような複雑なケースに対処するには、クローン作成方法を組み合わせたり、カスタムコードを書いたり、あるいは既存の実装を使用したりする必要があります(例:JavaScriptライブラリlodashの_.cloneDeep(obj))。
まとめ
オブジェクトは参照によって割り当てられ、コピーされます。つまり、変数は「オブジェクト値」ではなく、「参照」(メモリアドレス)を格納します。そのため、そのような変数をコピーしたり、関数引数として渡したりすると、オブジェクト自体ではなくその参照がコピーされます。
コピーされた参照によるすべての操作(プロパティの追加/削除など)は、同じ1つのオブジェクトに対して実行されます。
「実際の複製」(クローン)を作成するには、いわゆる「シャローコピー」(ネストされたオブジェクトは参照によってコピーされます)の場合はObject.assign
を使用するか、「ディープクローン」関数structuredClone
を使用するか、_.cloneDeep(obj)などのカスタムクローン実装を使用します。
コメント
<code>
タグを使用し、複数行の場合は<pre>
タグで囲み、10行を超える場合はサンドボックス(plnkr、jsbin、codepen…)を使用してください。