JavaScript で配列やオブジェクトをコピーしたり、複数をまとめて1つにしたりする場面はよくあります。そんなときに役立つのが、ピリオド3つで書くスプレッド構文(...)です。この記事では、配列のコピー・結合、オブジェクトのコピー・マージ(上書き)、関数の引数への展開といった使い方を、初心者にもわかるように順番に解説します。あわせて、見た目がよく似た残余引数(rest)との違いや、コピーが「浅い」ことによる注意点にも触れます。
目次
スプレッド構文(…)とは
スプレッド構文は、配列やオブジェクトの中身をその場で1つずつ展開するための書き方です。展開したい値の前にピリオドを3つ(...)付けて使います。「箱の中身を取り出して、別の箱に並べ直す」とイメージするとわかりやすいでしょう。
たとえば配列 [1, 2, 3] を ... で展開すると、1, 2, 3 という個々の値の並びになります。この性質を使って、コピー・結合・引数の受け渡しなどをすっきり書けるのがスプレッド構文の魅力です。
const numbers = [1, 2, 3];
// 配列を [ ] の中で展開すると、中身が1つずつ並ぶ
console.log([...numbers]); // [1, 2, 3]
// 関数の引数として展開することもできる
console.log(Math.max(...numbers)); // 3(Math.max(1, 2, 3) と同じ)
配列をコピーする
配列を別の変数に代入しただけでは、コピーにはなりません。const copy = original; のように書くと、両者は同じ配列を指すため、片方を変更すると、もう片方にも影響してしまいます。
スプレッド構文を使えば、中身を新しい配列に展開し直すことで、元とは別物のコピーを作れます。
const original = ["apple", "banana"];
// 中身を展開して、新しい配列を作る
const copy = [...original];
copy.push("cherry");
console.log(original); // ["apple", "banana"](元は変わらない)
console.log(copy); // ["apple", "banana", "cherry"]
copy に要素を追加しても original は変わっていません。元の配列を壊さずに加工したいときに便利な書き方です。
配列を結合する
複数の配列を1つにまとめたいときも、それぞれを ... で展開して並べるだけです。concat() メソッドでも同じことができますが、スプレッド構文は途中に要素を差し込めるなど、書き方の自由度が高いのが特徴です。
const fruits = ["apple", "banana"];
const vegetables = ["carrot", "tomato"];
// 2つの配列を展開して、1つの配列にまとめる
const food = [...fruits, ...vegetables];
console.log(food); // ["apple", "banana", "carrot", "tomato"]
// 途中に好きな要素を差し込むこともできる
const withEgg = [...fruits, "egg", ...vegetables];
console.log(withEgg); // ["apple", "banana", "egg", "carrot", "tomato"]
オブジェクトをコピー・マージする
スプレッド構文はオブジェクトにも使えます。波かっこ { } の中でオブジェクトを展開すると、そのプロパティが1つずつコピーされます。配列のときと同じく、元のオブジェクトを壊さずにコピーや更新ができます。
const user = { name: "Taro", age: 20 };
// プロパティを展開して、新しいオブジェクトを作る
const copy = { ...user };
console.log(copy); // { name: "Taro", age: 20 }
// 2つのオブジェクトをマージする
const profile = { city: "Tokyo" };
const merged = { ...user, ...profile };
console.log(merged); // { name: "Taro", age: 20, city: "Tokyo" }
マージのとき、同じ名前のプロパティがあると、後に書いたほうの値で上書きされます。この性質を利用すると、「元のオブジェクトをベースに、一部だけ値を変えた新しいオブジェクトを作る」といった更新がきれいに書けます。
const user = { name: "Taro", age: 20 };
// age だけを上書きした新しいオブジェクトを作る
const updated = { ...user, age: 21 };
console.log(user); // { name: "Taro", age: 20 }(元は変わらない)
console.log(updated); // { name: "Taro", age: 21 }
関数の引数に展開する
配列に入った値を、そのまま関数の引数として渡したいことがあります。このとき配列を ... で展開すると、各要素が個別の引数として渡されます。Math.max() のように、複数の引数を受け取る関数と組み合わせると便利です。
const scores = [82, 95, 67, 100];
// 配列を展開して引数に渡す
const highest = Math.max(...scores);
console.log(highest); // 100
// 展開しない場合は配列ごと渡ってしまい、正しく動かない
console.log(Math.max(scores)); // NaN
残余引数(rest)との違い
同じ ... を使う書き方に「残余引数(rest parameter)」があります。見た目はそっくりですが、役割は逆向きです。スプレッド構文が「まとまったものを展開してバラす」のに対し、残余引数は「バラバラの値を1つの配列にまとめる」ために使います。
| 書き方 | 使う場所 | 役割 |
|---|---|---|
| スプレッド構文 | 配列・オブジェクトの中、関数の呼び出し時 | 中身を1つずつ展開してバラす |
| 残余引数(rest) | 関数の引数を定義する場所 | 複数の引数を1つの配列にまとめる |
次の例では、関数を定義するときの引数に ...numbers と書いています。これが残余引数で、渡された複数の値が numbers という配列にまとまります。「... が関数の呼び出し側にあれば展開、関数の定義側(引数)にあればまとめ」と覚えると整理しやすいです。
// 引数の ...numbers は残余引数。渡された値を配列にまとめる
function sum(...numbers) {
return numbers.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2, 3)); // 6
console.log(sum(10, 20)); // 30
// 呼び出し側でスプレッド構文を使えば、配列を引数に展開できる
const values = [4, 5, 6];
console.log(sum(...values)); // 15
コピーは「浅い」ことに注意する
スプレッド構文によるコピーは、知っておきたい落とし穴があります。それは、コピーが「浅いコピー(shallow copy)」であるという点です。一番外側の階層は新しく作られますが、その中に入れ子(ネスト)になった配列やオブジェクトは、元と同じものを共有したままになります。
つまり、ネストした中身を書き換えると、コピー元にも影響が及びます。次の例を見てみましょう。
const user = {
name: "Taro",
address: { city: "Tokyo" }, // ネストしたオブジェクト
};
const copy = { ...user };
// ネストした address は元と共有されている
copy.address.city = "Osaka";
console.log(user.address.city); // "Osaka"(元まで変わってしまう)
console.log(copy.address.city); // "Osaka"
copy 側で address.city を変えただけなのに、user 側まで変わってしまいました。address オブジェクトが両者で共有されているためです。一番外側の name を書き換えても元には影響しませんが、ネストした部分は別物にならない、と覚えておきましょう。
ネストの中身まで完全に独立させたい場合は、ネストした部分も個別にスプレッド構文で展開する(例: { ...user, address: { ...user.address } })か、structuredClone() のような深いコピー(ディープコピー)の手段を使う必要があります。
まとめ
スプレッド構文(...)は、配列やオブジェクトの中身を展開して、コピー・結合・引数の受け渡しをすっきり書ける便利な構文です。
- 配列・オブジェクトを
[...arr]/{ ...obj }でコピーできる - 複数を展開して並べれば結合・マージができ、同名プロパティは後勝ちで上書きされる
- 関数の呼び出し時に
...を使うと、配列を個別の引数に展開できる - 関数の引数定義で使う残余引数(rest)は逆向きで、値を1つの配列にまとめる
- コピーは浅いコピーなので、ネストした中身は元と共有される点に注意する
まずは配列やオブジェクトのコピーから試して、元のデータを壊さずに加工する書き方に慣れていきましょう。