🐙

[JavaScript] スプレッド構文の使い所

2023/10/01に公開

1.配列やオブジェクトのコピー

既存のデータを変更せずに新しいデータを操作できます。

js
// 配列
const arr1 = [1, 2, 3];
const arr2 = [...arr1]; // => [1,2,3]

// オブジェクト
const obj1 = { x: 1, y: 2 };
const obj2 = {...obj1} // => { x: 1, y: 2 }

注意すべきこと

js
const obj1 = { x: 1, y: 2, z: {a: 1, b: 2} };
const obj2 = {...obj1}
obj2.z.a = 10

// この実装では
// obj2はObject { x: 1, y: 2, z: Object { a: 10, b: 2 } }
// obj1もObject { x: 1, y: 2, z: Object { a: 10, b: 2 } }
// となり、obj1の子オブジェクトの値が影響を受けています。

shallow copyを回避する方法

回避するためには深いコピー(deep copy)を行う必要があります。深いコピーでは、オブジェクトとそのすべての入れ子のプロパティも再帰的にコピーされます。

対応策①JSONを使用する

JSON.stringifyでオブジェクトを文字列に変換し、JSON.parseで新しいオブジェクトに変換します。この方法は完全な深いコピーが作成され、子オブジェクトの変更がコピー元に影響を与えません。

js
const obj1 = { x: 1, y: 2, z: {a: 1, b: 2} };
const obj2 = JSON.parse(JSON.stringify(obj1));
obj2.z.a = 10;
=> obj1 { x: 1, y: 2, z: Object { a: 1, b: 2 } }のまま
=> obj2 { x: 1, y: 2, z: Object { a: 10, b: 2 } }となります。

こちらのケース問題も抱えているので使用するときは気をつけなければなりません。
プロパティ値が循環参照している、またはプロパティ値がundefinedである場合はエラーになります。
絶対にエラーが起きないケースは使用しても問題ないですが、推奨できません。

対応策②子オブジェクト個別でコピー

子オブジェクトがコピーされない場合、意図的に個別でコピーする方法があります。

js
const obj1 = { x: 1, y: 2, z: {a: 1, b: 2} };
const obj2 = {...obj1, z: {...obj1.z}}
obj2.z.a = 10;
=> obj1 { x: 1, y: 2, z: Object { a: 10, b: 2 } }
=> obj2 { x: 1, y: 2, z: Object { a: 1, b: 2 } }となります。

柔軟性には欠けます。

対応策③structuredClone()を使用する

js
const obj1 = { x: 1, y: 2, z: {a: 1, b: 2} };
const obj2 = structuredClone(obj1);
obj2.z.a = 10;
=> obj1 { x: 1, y: 2, z: Object { a: 1, b: 2 } }のまま
=> obj2 { x: 1, y: 2, z: Object { a: 10, b: 2 } }となります。

ただし、メソッドが含まれているとエラーになります。

対応策④自作する

js
function deepCopy(obj) {
  if (typeof obj !== 'object' || obj === null) {
    // オブジェクトでない場合、またはnullの場合はそのまま返す
    return obj;
  }

  if (Array.isArray(obj)) {
    // 配列の場合は新しい配列を作成し、各要素を再帰的にコピー
    return obj.map(item => deepCopy(item));
  }

  // オブジェクトの場合は新しいオブジェクトを作成し、各プロパティを再帰的にコピー
  const copiedObj = {};
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      copiedObj[key] = deepCopy(obj[key]);
    }
  }

  return copiedObj;
}

const obj1 = {
  x: 1,
  y: [2, 3],
  z: { a: 4, b: [5, 6] }
};
const obj2 = deepCopy(obj1);
obj2.z.a = 10;
=> Object { x: 1, y: Array [2, 3], z: Object { a: 4, b: Array [5, 6] } }のまま
=> Object { x: 1, y: Array [2, 3], z: Object { a: 10, b: Array [5, 6] } }となります。

自作なのでボリュームありますが、使い勝手の良さからこの方法で対応するのが良いかなと思います。

2.配列(要素)やオブジェクト(プロパティ)の追加

js
// 配列
const arr1 = [0,1,2]
const arr2 = [3,4,5]
const arr3 = [...arr1, 4] // => [0, 1, 2, 3]

// オブジェクト
const obj1 = { x: 1, y: 2 }
const obj2 = {...obj1, z: 3} // => { x: 1, y: 2, z: 3 }

3.配列やオブジェクトのマージ

// 配列
const arr1 = [0,1,2]
const arr2 = [3,4,5]
const arr3 = [...arr1, arr2] // => [0, 1, 2, 3, 4, 5] 

// オブジェクト
const obj1 = { x: 1, y: 2 };
const obj2 = { y: 3, z: 4 };
const mergedObject = { ...obj1, ...obj2 } // => { x: 1, y: 3, z: 4 }

4.引数の伝播

可変長の引数を受け取りやすくなります。

function sum(...numbers) {
  return numbers.reduce((total, num) => total + num, 0);
}
sum(0,1,2,3,4,5) // => 15

4.特定の要素を変更しつつ新しい配列やオブジェクトを作成

const arr1 = [1, 2, 3];
const arr2 = [...arr1.slice(0, 1), 4, ...arr2.slice(2)]; // => [1, 4, 3]

Discussion