原文: How to clone an array in JavaScript

JavaScript では何をするにも様々なやり方があります。以前 JavaScript で pipe/compose を書く 10 の方法についての記事を書きましたが、今回は配列について説明します。

1. スプレッド構文 (シャローコピー)

ES6 が公開されて以来、これが最も人気のある方法です。簡潔な構文で、React や Redux のようなライブラリを使うときにその便利さがわかるでしょう。

numbers = [1, 2, 3];
numbersCopy = [...numbers];

注意: この方法は多次元配列を安全にコピーするものではありません。要素が配列やオブジェクトの場合、ではなく、参照によってコピーされます。

こちらは問題がありません。

numbersCopy.push(4);
console.log(numbers, numbersCopy);
// [1, 2, 3] and [1, 2, 3, 4]
// numbers 配列はそのままです。

こちらは問題があります。

nestedNumbers = [[1], [2]];
numbersCopy = [...nestedNumbers];

numbersCopy[0].push(300);
console.log(nestedNumbers, numbersCopy);
// [[1, 300], [2]]
// [[1, 300], [2]]
// 両方の配列内の数値が変化しました。どちらも同じ参照を共有しているからです。

2. 古き良き for() ループ (シャローコピー)

関数型プログラミングが流行っていることを考えると、これは1番人気がない方法でしょう。

純粋型でも非純粋型でも、宣言型でも命令型でも実行できます。

numbers = [1, 2, 3];
numbersCopy = [];

for (i = 0; i < numbers.length; i++) {
  numbersCopy[i] = numbers[i];
}

注意: この方法は多次元配列を安全にコピーするものではありません。=  演算子を使っているので、配列やオブジェクトについてはではなく、参照がコピーされます。

こちらは問題がありません。

numbersCopy.push(4);
console.log(numbers, numbersCopy);
// [1, 2, 3] and [1, 2, 3, 4]
// numbers 配列はそのままです。

こちらは問題があります。

nestedNumbers = [[1], [2]];
numbersCopy = [];

for (i = 0; i < nestedNumbers.length; i++) {
  numbersCopy[i] = nestedNumbers[i];
}

numbersCopy[0].push(300);
console.log(nestedNumbers, numbersCopy);
// [[1, 300], [2]]
// [[1, 300], [2]]
// 両方の配列内の数値が変化しました。どちらも同じ参照を共有しているからです。

3. 古き良き while() ループ (シャローコピー)

for と同じく、純粋型でも非純粋型でも、なんとかかんとか…とにかく動きます!

numbers = [1, 2, 3];
numbersCopy = [];
i = -1;

while (++i < numbers.length) {
  numbersCopy[i] = numbers[i];
}

注意: この方法も、配列やオブジェクトについてはではなく参照がコピーされます。

こちらは問題がありません。

numbersCopy.push(4);
console.log(numbers, numbersCopy);
// [1, 2, 3] and [1, 2, 3, 4]
// numbers 配列はそのままです。

こちらは問題があります。

nestedNumbers = [[1], [2]];
numbersCopy = [];

i = -1;

while (++i < nestedNumbers.length) {
  numbersCopy[i] = nestedNumbers[i];
}

numbersCopy[0].push(300);
console.log(nestedNumbers, numbersCopy);
// [[1, 300], [2]]
// [[1, 300], [2]]
// 両方の配列内の数値が変化しました。どちらも同じ参照を共有しているからです。

4. Array.map (シャローコピー)

現代の分野に戻ると、map  関数があります。map とは、数学用語に由来し、ある集合の構造を保ったまま別の種類の集合に変換する概念です。

平易な言葉で言うと、つまり Array.map は毎回同じ長さの配列を返すということです。

数値のリストを 2 倍にするには、mapdouble 関数を使用します。

numbers = [1, 2, 3];
double = (x) => x * 2;

numbers.map(double);

クローンするんじゃなかったの??

そうですね、この記事は配列のクローンについてです。配列を複製するには、呼び出された map 関数の中で、その要素をそのまま返します。

numbers = [1, 2, 3];
numbersCopy = numbers.map((x) => x);

もう少し数学的に言うと、(x) => x恒等関数と呼ばれます。これは与えられたパラメータが何であれ、それを返します。

map(identity) はリストのクローンを作成します。

identity = (x) => x;
numbers.map(identity);
// [1, 2, 3]

注意: この方法も、配列やオブジェクトについてはではなく参照がコピーされます。

5. Array.filter (シャローコピー)

この関数は map と同じように配列を返しますが、同じ長さであることは保証されません。

もし偶数の値をフィルターする場合、どうなるでしょうか?

[1, 2, 3].filter((x) => x % 2 === 0);
// [2]

入力された配列の長さは 3 でしたが、結果の長さは 1 です。

しかし filter の条件式が常に true を返すようにすれば、配列を複製できます!

numbers = [1, 2, 3];
numbersCopy = numbers.filter(() => true);

どの要素もテストに合格しているので、そのまま返されます。

注意: この方法も、配列やオブジェクトについてはではなく参照がコピーされます。

6. Array.reduce (シャローコピー)

本来 reduce は遥かに強力なことができるので、配列のクローンを作成するためだけに使うのは心苦しく感じます。でも、やってみましょう。

numbers = [1, 2, 3];

numbersCopy = numbers.reduce((newArray, element) => {
  newArray.push(element);

  return newArray;
}, []);

reduce は、リストをループしながら初期値を変更します。

ここでは、初期値は空の配列で、その中に各要素を詰めていきます。この配列は、次の反復処理で使用するために関数から返されなければなりません。

注意: この方法も、配列やオブジェクトについてはではなく参照がコピーされます。

7. Array.slice (シャローコピー)

slice は、指定した開始 / 終了インデックスに基づいて、配列のシャローコピーを返します。

もし、最初の 3 つの要素が必要な場合は次のとおりです。

[1, 2, 3, 4, 5].slice(0, 3);
// [1, 2, 3]
// Starts at index 0, stops at index 3

すべての要素が欲しい場合は、パラメータを指定しないでください。

numbers = [1, 2, 3, 4, 5];
numbersCopy = numbers.slice();
// [1, 2, 3, 4, 5]

注意: これはシャローコピーです。つまり、この方法も、配列やオブジェクトについてはではなく参照がコピーされます。

8. JSON.parse と JSON.stringify (ディープコピー)

JSON.stringify はオブジェクトを文字列に変換します。

JSON.parse は文字列をオブジェクトに変換します。

これらを組み合わせて、オブジェクトを文字列に変換し、それから逆の操作をすることで、データ構造を新しく作成できます。

注意: この方法であれば、深くネストしたオブジェクトや配列を安全にコピーできます!

nestedNumbers = [[1], [2]];
numbersCopy = JSON.parse(JSON.stringify(nestedNumbers));

numbersCopy[0].push(300);
console.log(nestedNumbers, numbersCopy);

// [[1], [2]]
// [[1, 300], [2]]
// この 2 つの配列は完全に分離されています!

9. Array.concat (シャローコピー)

concat は、配列を値や他の配列と結合します。

[1, 2, 3].concat(4); // [1, 2, 3, 4]
[1, 2, 3].concat([4, 5]); // [1, 2, 3, 4, 5]

何も指定しなかったり、空の配列を指定した場合は、シャローコピーが返されます。

[1, 2, 3].concat(); // [1, 2, 3]
[1, 2, 3].concat([]); // [1, 2, 3]

注意: この方法も、配列やオブジェクトについてはではなく参照がコピーされます。

10. Array.from (シャローコピー)

これは、任意の反復可能なオブジェクトを配列に変換することができます。配列を渡すと、シャローコピーが返されます。

numbers = [1, 2, 3];
numbersCopy = Array.from(numbers);
// [1, 2, 3]

注意: この方法も、配列やオブジェクトについてはではなく参照がコピーされます。

結論

楽しんでいただけましたか?

今回は 1 ステップで配列をクローンする方法を紹介しました。複数のメソッドやテクニックを使えば、もっといろいろな方法が見つかるはずです。