JavaScriptの基礎 - スプレッド構文
概要
スプレッド構文 (...) は、ES2015 (ES6) で導入された構文であり、配列やオブジェクトの要素を個別の値に展開するために使用する。
配列に対してスプレッド構文を使用すると、配列の全要素を個別の値として展開できる。
これにより、配列の結合、コピー、関数への引数展開を簡潔に記述することができる。
オブジェクトに対しても同様に使用でき、オブジェクトのプロパティを展開してコピーやマージを実現できる。
後から指定したプロパティが優先されるため、特定のプロパティのみを上書きするパターンとしても広く活用される。
スプレッド構文が行うコピーはシャローコピー (浅いコピー) であり、1レベル深くのみコピーする点に注意が必要である。
ネストされたオブジェクトや配列は参照がコピーされるため、元のオブジェクトと同じ参照を共有する。
Reactの状態管理においては、スプレッド構文はイミュータブルな更新パターンの中心的な構文として使用される。
useState で管理するオブジェクトや配列を更新する時には、スプレッド構文で新しいオブジェクトを生成することが必須である。
同じ ... 記法でも、使用する位置によって意味が異なる。
右辺や関数呼び出し時に使用した場合はスプレッド構文として「展開する」動作をし、左辺や関数パラメータで使用した場合は残余パターンとして 集約する 動作をする。
配列でのスプレッド構文
配列の展開
スプレッド構文を使用すると、配列の全要素を個別の値として展開できる。
展開した値の前後に追加の要素を並べることで、先頭・末尾への要素追加も簡潔に記述できる。
const arr = [1, 2, 3];
// 配列の要素を展開して新しい配列を作成する
const expanded = [...arr, 4, 5];
console.log(expanded); // [1, 2, 3, 4, 5]
// 先頭に要素を追加する
const withStart = [0, ...arr];
console.log(withStart); // [0, 1, 2, 3]
// 末尾に要素を追加する
const withEnd = [...arr, 4];
console.log(withEnd); // [1, 2, 3, 4]
// 中間に挿入する
const withMiddle = [0, ...arr, 4, 5];
console.log(withMiddle); // [0, 1, 2, 3, 4, 5]
配列のコピー (シャローコピー)
スプレッド構文を使用すると、配列のシャローコピーを簡潔に作成できる。
コピーされた配列は元の配列とは独立した別の配列であるため、要素の追加・削除は元の配列に影響しない。
const original = [1, 2, 3];
// スプレッド構文でシャローコピーを作成する
const copy = [...original];
// copyを変更しても、originalは変化しない
copy.push(4);
console.log(original); // [1, 2, 3]
console.log(copy); // [1, 2, 3, 4]
// 配列の参照比較
console.log(original === copy); // false (別の配列オブジェクト)
配列の結合
スプレッド構文を使用すると、複数の配列を結合した新しい配列を作成できる。
Array.prototype.concat の代替として使用でき、より直感的に記述できる。
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const arr3 = [7, 8, 9];
// 2つの配列を結合する
const combined = [...arr1, ...arr2];
console.log(combined); // [1, 2, 3, 4, 5, 6]
// 3つ以上の配列を結合する
const all = [...arr1, ...arr2, ...arr3];
console.log(all); // [1, 2, 3, 4, 5, 6, 7, 8, 9]
// 結合時に追加の要素も含められる
const withExtra = [...arr1, 0, ...arr2];
console.log(withExtra); // [1, 2, 3, 0, 4, 5, 6]
オブジェクトでのスプレッド構文
オブジェクトの展開
スプレッド構文はオブジェクトのプロパティを展開して、新しいオブジェクトを作成するためにも使用できる。
この機能はES2018で導入された。
const obj = { a: 1, b: 2 };
// オブジェクトのプロパティを展開して、新しいオブジェクトを作成する
const expanded = { ...obj, c: 3 };
console.log(expanded); // { a: 1, b: 2, c: 3 }
// 先頭にプロパティを追加する
const withPrefix = { x: 0, ...obj };
console.log(withPrefix); // { x: 0, a: 1, b: 2 }
オブジェクトのコピー (シャローコピー)
スプレッド構文を使用すると、オブジェクトのシャローコピーを作成できる。
コピーしたオブジェクトへのプロパティ追加・変更は、元のオブジェクトに影響しない。
ただし、値がオブジェクトや配列のプロパティについては参照のみがコピーされる点に注意が必要である。
const original = { a: 1, b: 2 };
// スプレッド構文でシャローコピーを作成する
const copy = { ...original };
// copyのプロパティを変更しても、originalは変化しない
copy.a = 100;
console.log(original); // { a: 1, b: 2 }
console.log(copy); // { a: 100, b: 2 }
// オブジェクトの参照比較
console.log(original === copy); // false (別のオブジェクト)
オブジェクトのマージ
スプレッド構文を使用すると、複数のオブジェクトを1つにマージできる。
Object.assign の代替として使用でき、より簡潔に記述できる。
const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };
const obj3 = { e: 5, f: 6 };
// 2つのオブジェクトをマージする
const merged = { ...obj1, ...obj2 };
console.log(merged); // { a: 1, b: 2, c: 3, d: 4 }
// 3つ以上のオブジェクトをマージする
const all = { ...obj1, ...obj2, ...obj3 };
console.log(all); // { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6 }
プロパティの上書き
複数のオブジェクトをスプレッド構文でマージする場合、同名のプロパティは後から指定した値で上書きされる。
この性質を利用すると、オブジェクトの特定のプロパティのみを更新した新しいオブジェクトを簡潔に作成できる。
const obj = { a: 1, b: 2, c: 3 };
// 特定のプロパティを上書きする
// 後から指定したプロパティが優先される
const updated = { ...obj, b: 20 };
console.log(updated); // { a: 1, b: 20, c: 3 }
// 前に指定した場合は、objのプロパティに上書きされる
const overwritten = { b: 0, ...obj };
console.log(overwritten); // { b: 2, a: 1, c: 3 } ← bはobjの値に上書きされる
// 複数プロパティを同時に上書きする
const multiUpdate = { ...obj, a: 10, c: 30 };
console.log(multiUpdate); // { a: 10, b: 2, c: 30 }
関数引数での展開
スプレッド構文を使用すると、配列の要素を関数の個別の引数として展開できる。
Function.prototype.apply の代替として使用でき、より直感的に記述できる。
// Math.maxへの展開
const numbers = [1, 5, 3, 9, 2];
// applyを使用した従来の方法
const maxOld = Math.max.apply(null, numbers);
// スプレッド構文を使用した方法 (推奨)
const max = Math.max(...numbers);
console.log(max); // 9
const min = Math.min(...numbers);
console.log(min); // 1
// 自作関数への引数展開
function sum(a, b, c) {
return a + b + c;
}
const args = [1, 2, 3];
console.log(sum(...args)); // 6
// 一部を固定して残りを展開する
function greet(greeting, firstName, lastName) {
return greeting + ", " + firstName + " " + lastName + "!";
}
const nameParts = ["Alice", "Smith"];
console.log(greet("Hello", ...nameParts)); // "Hello, Alice Smith!"
シャローコピーの注意点
ネストしたオブジェクトの問題
スプレッド構文によるコピーはシャローコピーであり、1レベル深くのみコピーする。
ネストされたオブジェクトや配列は参照のみがコピーされるため、コピー先を変更すると元のオブジェクトにも影響が生じる。
const original = {
name: "Alice",
address: { city: "Tokyo", zip: "100-0001" }
};
// シャローコピーを作成する
const copy = { ...original };
// トップレベルのプロパティは独立している
copy.name = "Bob";
console.log(original.name); // "Alice" (変化しない)
// ネストされたオブジェクトは参照を共有している
copy.address.city = "Osaka";
console.log(original.address.city); // "Osaka" (元のオブジェクトも変化する!)
ネストしたオブジェクトも安全にコピーするには、内側のオブジェクトに対しても個別にスプレッド構文を適用する。
const original = {
name: "Alice",
address: { city: "Tokyo", zip: "100-0001" }
};
// ネストしたオブジェクトにも個別にスプレッドを適用する
const safeCopy = {
...original,
address: { ...original.address, city: "Osaka" }
};
console.log(safeCopy.address.city); // "Osaka"
console.log(original.address.city); // "Tokyo" (変化しない)
ディープコピーの方法
ネストの深さに関わらず全てのレベルを完全にコピーするディープコピーが必要な場合、以下の方法を使用する。
structuredClone()(推奨)- モダンブラウザおよびNode.js v17以降で使用できる組み込み関数である。
- 循環参照、
Date、Map、Set、ArrayBuffer等の複雑なデータ型にも対応している。
JSON.parse(JSON.stringify())- 関数、
undefined、Symbol、Dateオブジェクト (文字列に変換される)、循環参照を含むオブジェクトには使用できない。 - シンプルなJSONデータに対してのみ有効な方法である。
- 関数、
const original = {
name: "Alice",
address: { city: "Tokyo" },
hobbies: ["reading", "coding"]
};
// structuredClone() によるディープコピー (推奨)
const deepCopy1 = structuredClone(original);
deepCopy1.address.city = "Osaka";
deepCopy1.hobbies.push("gaming");
console.log(original.address.city); // "Tokyo" (変化しない)
console.log(original.hobbies); // ["reading", "coding"] (変化しない)
// JSON.parse(JSON.stringify()) によるディープコピー
// 関数・undefined・Symbol・循環参照が含まれる場合は使用不可
const deepCopy2 = JSON.parse(JSON.stringify(original));
deepCopy2.address.city = "Nagoya";
console.log(original.address.city); // "Tokyo" (変化しない)
Reactでのイミュータブル更新パターン
stateの更新
Reactでは、useState で管理するオブジェクトを更新する時に、スプレッド構文を使用する。
オブジェクトや配列を直接変更 (ミューテーション) すると、Reactが変更を検知できず再描画が発生しない。
スプレッド構文で新しいオブジェクトを生成することにより、Reactが変更を検知して正しく再描画される。
import { useState } from "react";
function UserProfile() {
const [user, setUser] = useState({ name: "Alice", age: 30, city: "Tokyo" });
// 特定のプロパティのみを更新する
function handleAgeChange() {
setUser({ ...user, age: 31 });
}
// 関数形式 (前のstateを確実に参照できる、推奨)
function handleCityChange(newCity) {
setUser(prev => ({ ...prev, city: newCity }));
}
// ネストしたオブジェクトの更新
const [profile, setProfile] = useState({
name: "Alice",
address: { city: "Tokyo", zip: "100-0001" }
});
function handleAddressChange(newCity) {
setProfile(prev => ({
...prev,
address: { ...prev.address, city: newCity }
}));
}
return (
<div>
<p>{user.name}, {user.age}歳, {user.city}</p>
<button onClick={handleAgeChange}>年齢を更新</button>
<button onClick={() => handleCityChange("Osaka")}>都市を変更</button>
</div>
);
}
配列のイミュータブル操作
Reactで配列の state を更新する場合も、push・splice 等の破壊的メソッドは使用しない。
スプレッド構文、map、filter を使用して新しい配列を生成することが必須である。
import { useState } from "react";
function TodoList() {
const [items, setItems] = useState([
{ id: 1, text: "買い物をする", done: false },
{ id: 2, text: "掃除をする", done: false }
]);
// 要素を追加する (pushの代替)
function handleAdd(newItem) {
setItems([...items, newItem]);
}
// 要素を削除する (spliceの代替)
function handleDelete(targetId) {
setItems(items.filter(item => item.id !== targetId));
}
// 特定の要素を更新する (mapとスプレッドを組み合わせる)
function handleUpdate(targetId, newText) {
setItems(items.map(item =>
item.id === targetId ? { ...item, text: newText } : item
));
}
// 完了状態をトグルする
function handleToggle(targetId) {
setItems(items.map(item =>
item.id === targetId ? { ...item, done: !item.done } : item
));
}
return (
<ul>
{items.map(item => (
<li key={item.id}>
{item.text}
<button onClick={() => handleDelete(item.id)}>削除</button>
<button onClick={() => handleToggle(item.id)}>完了</button>
</li>
))}
</ul>
);
}
下表に、破壊的メソッドとイミュータブルな代替方法を示す。
| 操作 | 破壊的メソッド (使用不可) | イミュータブルな代替 |
|---|---|---|
| 末尾に追加 | push(item) |
[...arr, item]
|
| 先頭に追加 | unshift(item) |
[item, ...arr]
|
| 要素を削除 | splice(i, 1) |
arr.filter(item => item.id !== id)
|
| 要素を更新 | arr[i] = newItem |
arr.map(item => item.id === id ? { ...item, ...updates } : item)
|
| ソート | sort() |
[...arr].sort()
|
| 逆順 | reverse() |
[...arr].reverse()
|
スプレッド構文と残余パターンの違い
スプレッド構文と残余パターンは、どちらも同じ ... 記法を使用するが、使用する位置によって動作が異なる。
- スプレッド構文 (展開)
- 右辺または関数呼び出し時に使用し、配列やオブジェクトを個別の値に「展開する」動作をする。
- 残余パターン (集約)
- 左辺または関数パラメータで使用し、残りの要素を配列やオブジェクトに「集約する」動作をする。
// スプレッド構文 - 展開する (右辺、関数呼び出し時)
const arr = [1, 2, 3];
const expanded = [...arr, 4, 5]; // 配列を展開する
const obj1 = { a: 1 };
const merged = { ...obj1, b: 2 }; // オブジェクトを展開する
Math.max(...arr); // 関数引数として展開する
// 残余パターン : 集約する (左辺、関数パラメータ)
// 配列の分割代入での残余パターン
const [first, ...rest] = [1, 2, 3, 4, 5];
console.log(first); // 1
console.log(rest); // [2, 3, 4, 5]
// オブジェクトの分割代入での残余パターン
const { a, ...others } = { a: 1, b: 2, c: 3 };
console.log(a); // 1
console.log(others); // { b: 2, c: 3 }
// 関数パラメータでの残余パターン
function sum(...args) {
return args.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2, 3, 4, 5)); // 15
下表に、スプレッド構文と残余パターンの違いを示す。
| 項目 | スプレッド構文 | 残余パターン |
|---|---|---|
| 記法 | ... |
...
|
| 動作 | 展開する (1つ → 複数) | 集約する (複数 → 1つ) |
| 使用位置 | 右辺、関数呼び出し時 | 左辺、関数パラメータ |
| 使用例 | [...arr, 4]、fn(...args) |
const [a, ...rest] = arr、function fn(...args)
|
関連情報
- JavaScriptの基礎 - オブジェクトリテラル
- オブジェクトの作成と操作、プロパティアクセス、短縮記法
- JavaScriptの基礎 - 分割代入
- オブジェクト/配列の分割代入、デフォルト値、残余パターン
- JavaScriptの基礎 - 関数宣言と関数式
- 関数宣言と関数式、デフォルト引数、残余引数