8日 五月 2021

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

オブジェクトとプリミティブの基本的な違いの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つのオブジェクトは、同じオブジェクトのときだけ等しくなります。

例えば、2つの変数が同じオブジェクトを参照しているとき、それらは等しいです:

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

(*) はエラーを起こすように見えるかもしれませんが、それらは全く問題ありません。constuser 自身の値を固定するからです。そしてここで user は常に同じオブジェクトへの参照を保持します。行 (*) はオブジェクトの 内側 へ行っており、user の再代入ではありません。

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

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

Summary

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

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

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

チュートリアルマップ

コメント

コメントをする前に読んでください…
  • 自由に記事への追加や質問を投稿をしたり、それらに回答してください。
  • 数語のコードを挿入するには、<code> タグを使ってください。複数行の場合は <pre> を、10行を超える場合にはサンドボックスを使ってください(plnkr, JSBin, codepen…)。
  • 記事の中で理解できないことがあれば、詳しく説明してください。