2021年12月15日

オブジェクト参照とコピー

オブジェクトとプリミティブの基本的な違いの1つは、オブジェクトは “参照によって” 格納されたりコピーされることです。それに対して、プリミティブ値(文字列、数値、真偽値 など)は、常に “値” としてコピーされます。

値をコピーするときに何が起きているのか少し詳しくみることで、簡単に理解できます。

文字列のような、プリミティブから始めましょう。

ここでは、message のコピーを phrase に格納します。:

let message = "Hello!";
let phrase = message;

結果、2つの独立した変数ができます。それぞれが文字列 "Hello!" を格納しています。

とても明白な結果ですね。

オブジェクトはそうではありません。

オブジェクトに割り当てられた変数は、オブジェクト自体ではなく、“メモリ上のアドレス”、言い換えるとオブジェクトへの “参照” を格納します。

このような変数の例を見てみましょう:

let user = {
  name: "John"
};

そして、これはメモリ上に実際にどのように格納されているかを示します:

オブジェクトはメモリ上のどこか(図の右側)に格納され、user 変数(図の左側)は、そこへの “参照” を持ちます。

user のようなオブジェクト変数は、オブジェクトのアドレスが記載された用紙と考えることができます。

オブジェクトへのアクションを行う際(例えば プロパティ user.name を取得する)JavaScript エンジンはそのアドレスにあるものを見て、実際のオブジェクト上で操作を実行します。

これが重要な理由です。

オブジェクト変数がコピーされた場合、参照はコピーされます。が、オブジェクト自体は複製されません。

例:

let user = { name: "John" };

let admin = user; // 参照のコピー

今、2つの変数があり、それぞれが同じオブジェクトへの参照を保持しています:

ご覧の通り、依然として1つのオブジェクトですが、今はそのオブジェクトを参照している変数は2つです。

どちらの変数を使用しても、オブジェクトにアクセスでき、その内容を変更することができます:

let user = { name: 'John' };

let admin = user;

admin.name = 'Pete'; // "admin" の参照で変更されました

alert(user.name); // 'Pete', "user" の参照からも変更が確認できます

これは、2つの鍵があるキャビネットで、そのうちの1つ(admin)を使用して中身を取得したり変更したかのように捉えることができます。その後、別の鍵(user)を使って、同じキャビネットを開き、変更されたコンテンツにアクセスできます。

参照による比較

2つのオブジェクトが等しいのは、それらが同一のオブジェクトである場合のみです。

例えば、ここではaとbは同じオブジェクトを参照しているため、それらは等しいということになります:

let a = {};
let b = a; // 参照のコピー

alert( a == b ); // true, 両方の変数は同じオブジェクトを参照しています
alert( a === b ); // true

そして、2つの独立したオブジェクトは等しくはありません。たとえどちらも空で、同じように見えるとしてもです:

let a = {};
let b = {}; // 2つの独立したオブジェクト

alert( a == b ); // false

obj1 > obj2 のような比較、もしくは反対にプリミティブ obj == 5 のような比較では、オブジェクトはプリミティブに変換されます。私たちはオブジェクト変換がどのように動作するのか、この後すぐに学ぶでしょう。ただし、真実を言うと、このような比較はほとんど必要とされず、通常はコードの誤りです。

クローンとマージ, Object.assign

前記のように、オブジェクト変数のコピーは、同一オブジェクトへの参照をもう1つ作ります。

しかし、オブジェクトを複製する必要がある場合はどうでしょうか?独立したコピー、クローンを作るには?

それは可能ですが、JavaScriptにはクローンのための組み込みメソッドがないため少し手間がかかります。しかし、それが必要になることはほとんどないでしょう。ほとんどの場合、参照によるコピーで十分だからです。

しかし、もし本当にそうしたい場合は、新しいオブジェクトを作り、プリミティブなレベルでそのプロパティを繰り返しコピーしていくことで、既存のものの構造を複製する必要があります。

このようになります:

let user = {
  name: "John",
  age: 30
};

let clone = {}; // 新しい空オブジェクト

// すべての user プロパティをその中にコピーしましょう
for (let key in user) {
  clone[key] = user[key];
}

// 今、clone は完全に独立したクローンです
clone.name = "Pete"; // その中のデータを変更

alert( user.name ); // 依然としてオリジナルのオブジェクトは John

また、そのために、Object.assign 関数を使うことができます。

構文はこうです:

Object.assign(dest[, src1, src2, src3...])
  • 最初の引数 dest はターゲットとなるオブジェクトです。
  • つづく引数 src1, ..., srcN (必要なだけ) は元となるオブジェクトです。
  • すべてのオブジェクト src1, ..., srcN のプロパティを dest にコピーします。言い換えると、2つ目から始まる全ての引数のプロパティは、最初の引数のオブジェクトにコピーされます。
  • dest を返します。

例えば、いくつかのオブジェクトを1つにマージするために使います:

let user = { name: "John" };

let permissions1 = { canView: true };
let permissions2 = { canEdit: true };

// permissions1 and permissions2 のすべてのプロパティを user にコピー
Object.assign(user, permissions1, permissions2);

// now user = { name: "John", canView: true, canEdit: true }

もし、既に同じプロパティ名のものをもっていた場合、上書きします:

let user = { name: "John" };

Object.assign(user, { name: "Pete" });

alert(user.name); // now user = { name: "Pete" }

また、単純なクローンをする場合の for..in ループ処理を置き換えるために、Object.assign を使うこともできます。

let user = {
  name: "John",
  age: 30
};

let clone = Object.assign({}, user);

これは user のすべてのプロパティを空のオブジェクトにコピーし、返します。

例えば、spread syntax clone = {...user} を使用するなど、オブジェクトをクローンする方法は他にもあります。これらはチュートリアルの後半で説明します。

ネストされたクローン

今までは、user のすべてのプロパティがプリミティブであると仮定していましたが、プロパティは他のオブジェクトの参照になることもあります。それらはどうなるでしょう?

このような場合です:

let user = {
  name: "John",
  sizes: {
    height: 182,
    width: 50
  }
};

alert( user.sizes.height ); // 182

今、user.sizes はオブジェクトであり、参照によるコピーがされるため、clone.sizes = user.sizes というコピーでは不十分です。なので、cloneuser は同じ sizes を共有します:

このようになります:

let user = {
  name: "John",
  sizes: {
    height: 182,
    width: 50
  }
};

let clone = Object.assign({}, user);

alert( user.sizes === clone.sizes ); // true, 同じオブジェクト

// user と clone は sizes を共有します
user.sizes.width++;       // 一方からプロパティを変更します
alert(clone.sizes.width); // 51, 他方から変更した結果が見えます

これを修正するには、user[key] の各値を調べ、それがオブジェクトの場合はその構造も複製するクローンのループを使用する必要があります。 これは “ディープクローン(ディープコピー)” と呼ばれます。

その実現のためには、再帰を使用する、あるいは、車輪の再発明をしないために、例えば既存の JavaScript ライブラリlodash にある_.cloneDeep(obj) を利用することができます。

Const オブジェクトは変更可能です

オブジェクトを参照として格納する重要な副作用は、const として宣言されたオブジェクトは変更 できます

例:

const user = {
  name: "John"
};

user.name = "Pete"; // (*)

alert(user.name); // Pete

(*) はエラーを起こすように見えるかもしれませんが、そうではありません。const である user は、常に同じオブジェクトを参照しなければなりませんが、そのオブジェクトのプロパティは自由に変更可能だからです。

つまり、const useruser=... のように全体を設定しようとした場合にのみエラーになります。

とはいえ、どうしてもオブジェクトのプロパティを定数にしたい場合はそれも可能ですが、全く異なるメソッドを使用します。これについては、プロパティフラグとディスクリプタ の章で説明します。

Summary

オブジェクトの割り当てやコピーは、参照によって行われます。つまり、変数には “オブジェクトの値” ではなく、 値への “参照” (メモリ上のアドレス)が格納されています。従って、このような変数をコピーしたり、それを関数の引数として渡すと、オブジェクトそのものではなく、その参照がコピーされます。

コピーされた参照を介したすべての操作(プロパティの追加/削除など)は、同じ単一のオブジェクトに対して実行されます。

“本当のコピー” (クローン) を作るには、Object.assign を使った “shallow copy”(浅いコピー、ネストされたオブジェクトは参照がコピーされる)を行うか、 _.cloneDeep(obj) のような “deep cloning” 関数を使います。

チュートリアルマップ