はじめに
- この記事ではJavaScriptの配列・オブジェクトを正しくコピー(複製)する方法を紹介します
- 初歩的な内容ですが、筆者が初見殺しに会った為、備忘録として共有します
この記事の対象者
- 現在JavaScriptを学ばれている方
- これからJavaScriptを学ばれる方
NGパターン
以下は配列「arr1」のコピー(複製)を作るソースコードです。
const arr1 = [10, 20];
const arr2 = arr1; // 代入によるコピー
console.log(arr1);
console.log(arr2);
以下、実行結果です。
> [10, 20]
> [10, 20]
コピーは出来てそうですね。
ただ、これは危険な書き方です。
何が危険なのでしょうか?それは...
配列のイコール(=)による代入は、コピー元を参照してしまう
上述NGパターンについて、arr2[0]の値を変えてみます。
const arr1 = [10, 20];
const arr2 = arr1;
arr2[0] = 30;
console.log(arr1);
console.log(arr2);
以下、実行結果です。
> [30, 20]
> [30, 20]
コピー先(arr2)の値を変えるとコピー元(arr1)の値も変わってしまう、
つまりコピー元を参照していますね!!
これは予期せぬ挙動と成り得るので修正しましょう。
じゃあどうすれば?
いくつか方法はありますが、この記事ではその中で最も直感的に分かり易かった
スプレッド構文(後ほど補足します)を使用します。
- スプレッド構文以外の手法について
まずは修正後のソースコードを見てみましょう。
const arr1 = [10, 20];
const arr2 = [...arr1]; // スプレッド構文
arr2[0] = 30;
console.log(arr1);
console.log(arr2);
以下、実行結果です。
> [10, 20]
> [30, 20]
コピー先(arr2)のみが変更されました。めでたしめでたし...
...と思うじゃん? ※2023/06/26追記
有識者の方々にご指摘頂きましたので追記です。
実は、スプレッド構文は1次元までのコピー(シャローコピー)でした。
その為、それより深い階層では以下のようにコピー元を参照してしまいます。
const arr1 = [10, [20, 30]];
const arr2 = [...arr1];
arr2[1][0] = 100;
console.log(arr1); // [ 10, [ 100, 30 ] ]
console.log(arr2); // [ 10, [ 100, 30 ] ]
これを回避する為に、全ての階層のコピー(ディープコピー)を行う必要があります。
それを実現するのが...
structuredClone() ※2023/06/26追記
以下、structuredClone()を使用したソースコードです。
シンプル且つ直感的で分かりやすいですね!!
const arr1 = [10, [20, 30]];
const arr2 = structuredClone(arr1);
arr2[1][0] = 100;
console.log(arr1); // [ 10, [ 20, 30 ] ]
console.log(arr2); // [ 10, [ 100, 30 ] ]
コピー先(arr2)の2階層目のみが変更されました。めでたしめでたし。
補足
スプレッド構文とは?
- スプレッド構文は本来、配列・オブジェクトの前に「...」を記述することで、展開した要素内の値を出力するものです
- ES2015(ES6) で追加された機能で、現在では全てのブラウザで対応しています
const arr1 = [10, 20];
console.log(arr1); // [10, 20]
console.log(...arr1); // 10 20
今回紹介したスプレッド構文を使って配列をコピーする方法は、展開した値から新しい配列が生成されたということですね。
オブジェクトも代入を行うと、コピー元を参照してしまう
配列同様にスプレッド構文を使用することで、1階層であればコピー元が参照されることを回避できます。
※ディープコピーを行う場合はstructuredClone()を使いましょう
- 代入によるコピー(オブジェクト)
const obj1 = {val1: 10, val2: 20};
const obj2 = obj1; // 代入によるコピー
obj2.val1 = 30;
console.log(obj1); // {val1: 30, val2: 20}
console.log(obj2); // {val1: 30, val2: 20}
- スプレッド構文を使用したコピー(オブジェクト)
const obj1 = {val1: 10, val2: 20};
const obj2 = {...obj1};
obj2.val1 = 30;
console.log(obj1); // {val1: 10, val2: 20}
console.log(obj2); // {val1: 30, val2: 20}
まとめ
- 配列・オブジェクトはイコール(=)で代入すると、コピー元を参照してしまう
- 代入ではなく、
スプレッド構文structuredClone()を使おう!