スプレッド構文によるクローンの作成

ここでは TypeScript のスプレッド構文を用いて、オブジェクトのクローンを作成する方法とその注意点について説明します。

最初に結論をいえば、スプレッド構文はすごく便利だけど、スプレッド構文ではプロパティをシャローコピーするだけなので注意してね、ということです。

スプレッド構文の基礎については「スプレッド構文」をみてください。

クローンを作る動機は?

「同じ値を持つオブジェクトを作成する」というのをどのように実現したら良いか考えてみましょう。

次の例をみてください。

ここでは変数 p1 に名前 (firstName, lastName) と年齢 (age) をセットして、 それを変数 p2 にコピーしています。

let p1 = {
    age: 30,
    firstName: 'Ichiro',
    lastName: 'Suzuki',
};

console.log(`p1: ${JSON.stringify(p1)}`); // p1 を出力

let p2 = p1; // 変数 p2 に p1 を代入

console.log(`p2: ${JSON.stringify(p2)}`); // p2 を出力

p2.lastName = 'Yamada'; // p2 を書き換え

console.log(`p1: ${JSON.stringify(p1)}`); // p1 を出力
console.log(`p2: ${JSON.stringify(p2)}`); // p2 を出力

実行結果は次のようになりました。

p1: {"age":30,"firstName":"Ichiro","lastName":"Suzuki"}
p2: {"age":30,"firstName":"Ichiro","lastName":"Suzuki"}
p1: {"age":30,"firstName":"Ichiro","lastName":"Yamada"}
p2: {"age":30,"firstName":"Ichiro","lastName":"Yamada"}

p2.lastNameYamada と書き換えたつもりが、 p1.lastName も書き換えられています。

なぜこのようなことになるかというと、p1p2 が全く同じモノを参照しているからです。 上のコードでは p2 = p1 としているので当然といえば当然です。

元のデータに影響を及ぼすことなく、あるデータのコピーを作るには、単なる参照のコピーではダメなのです。 「同じデータを持つ、異なるモノ」を作成しなければなりません。

スプレッド構文によるクローンの作成

「同じ値を持つ違うモノ」のことを一般的にクローン (clone) などと呼びます。 TypeScript で、あるオブジェクトのクローンを作るにはどうしたらよいでしょうか。

まず、ひとつの方法として次のように、それぞれのフィールド毎に値をコピーすることができます。

let p2 = {
    age: p1.age,
    firstName: p1.firstName,
    lastName: p1.lastName,
};

この方法で p2p1 のクローンになります。 つまり値は同じで、実態は異なるモノになります。

これで用を足すには足すのですが、しかし、困ることも出てきます。

フィールドの数だけ、ひとつひとつコピーするコードを書かなければならないので、 フィールド数が増えた時に面倒な上に、間違う可能性も大きくなります。元のデータにフィールドが増えた時に、 クローンを作る箇所全てを直す必要ができてしまい、コードの一貫性を保つことが難しくなります。

一歩間違えればあるオブジェクトにはこのフィールドがあるけど、こっちにはない、といった厄介な問題が発生してしまいます。

ここで便利に使えるのが、スプレッド構文です。

さっそく、スプレッド構文を使って上のコードを書き換えてみましょう。

スプレッド構文はドット (.) を3つ続けて記述します。

スプレッド構文を使うと上と同じことが一行で書けます。

let p2 = {...p1}; // クローンの作成

なんと、この一行で上に書いたような、それぞれのフィールドの値をコピーするということが実現できます。

このように書いておけば、p1 にフィールドが増えた時にも自動的に p2 にも新しいフィールドが作成されることになります。

こうして作成した新しいオブジェクトは、元のオブジェクトと値は同じでも実体は別のモノになります。すなわち、クローンが作成された、というわけです。

このように、スプレッド構文を使うとそれぞれのフィールドをバラバラにして、せっせとひとつひとつコピーして回る手間が省けるのです。

ディープコピーされないことに注意

ここまで、スプレッド構文を使ってクローンを作成する方法をみたわけですが、ひとつ重要な注意点があります。

次の例をみてください。変数 p1 のクローンを p2 として作成しています。

ポイントは配列のプロパティを含むところです。

const p1 = {
    favoriteFoods: ['Chinese', 'Italian'],
    name: 'Suzuki',
};

console.log('--- 1 ---');

const p2 = {...p1};

console.log(p1);
console.log(p2);

console.log('--- 2 ---');

p2.name = 'Yamada';

console.log(p1);
console.log(p2);

console.log('--- 3 ---');

p2.favoriteFoods[1] = 'Ramen';

console.log(p1);
console.log(p2);

この結果は、次のようになります。最後の --- 3 --- 以下で、p1 の favoriteFoods も Ramen に書き換えられています。

--- 1 ---
{ favoriteFoods: [ 'Chinese', 'Italian' ], name: 'Suzuki' }
{ favoriteFoods: [ 'Chinese', 'Italian' ], name: 'Suzuki' }
--- 2 ---
{ favoriteFoods: [ 'Chinese', 'Italian' ], name: 'Suzuki' }
{ favoriteFoods: [ 'Chinese', 'Italian' ], name: 'Yamada' }
--- 3 ---
{ favoriteFoods: [ 'Chinese', 'Ramen' ], name: 'Suzuki' }
{ favoriteFoods: [ 'Chinese', 'Ramen' ], name: 'Yamada' }

つまり、配列の部分に関しては、同じ要素を持つ異なる配列のコピー (すなわち配列のクローン) が作られたのではなく、同じ配列への参照のコピーが行われています。

ここでは配列の例をあげましたが、文字列や数値といったプリミティブな型以外のオブジェクトについても、同様の動きになります。 スプレッド構文ではプロパティとしてセットされたオブジェクトのプロパティまで辿って、データをコピーすることはありません。

このようなコピーを浅いコピー (シャローコピー、Shallow copy) といいます。

一方、プロパティとしてセットされた配列の値まで辿りコピーするとか、プロパティのオブジェクトのプロパティまでコピーするといった動作をするコピーを 深いコピー (ディープコピー、deep copy) といいます。

スプレッド構文はシャローコピーになります。配列やオブジェクトなど、もともと参照を保持していた箇所はそのまま、コピー先に持ち越されてしまいます。 それに注意して使わないと思わぬ不具合を持ち込んでしまうことになりますので気をつけましょう。

ちなみに、上の例ではクローンを作る際、次のように配列のところだけ、スプレッド構文を書き加えることで、 配列の箇所もクローンを作ることができます。(が、手書きなので全部の問題が解決できるわけではないですが、一応)

const p2 = {
    ...p1,
    favoriteFoods: [...p1.favoriteFoods]
};

以上、スプレッド構文を使ってクローンを作成するときの注意点について説明しました。

念のためにいえば、実際の開発の時には lodash などのライブラリを利用して、ディープクローンを作るのが現実的だったりします。 この点については、「lodash によるディープコピー」をみてください。

ここまでお読みいただき、誠にありがとうございます。SNS 等でこの記事をシェアしていただけますと、大変励みになります。どうぞよろしくお願いします。

© 2024 JavaScript 入門