同じ構文なのに役割は2つ
モダンなJavaScriptのコードを読んでいると、いたるところで ...(ドット3つ)を見かけます。この演算子、書く場所によって正反対の働きをするのがポイントです。一度パターンを掴んでしまえば、... の使い方はすべて次の2つに分類できます。
- Rest(レスト): 受け取る 側で
...nameと書くと、複数の値を1つの配列やオブジェクトにまとめます。 - Spread(スプレッド): 渡す 側で
...valueと書くと、配列やオブジェクトを個々の要素に展開します。
これさえ押さえれば大丈夫。ここから先は、それぞれの具体例と、実際によく使うパターンの紹介です。
Restパラメータ:引数をまとめて受け取る
関数定義で使う ... は、いくつ渡されても引数を1つの配列として受け取れる「可変長引数」の仕組みです。
nums は普通の配列です。.map も .filter も使えますし、.length で要素数も取れます。別の関数に渡すこともできる。要するに、配列でできることは何でもできます。
Restパラメータは通常の引数と組み合わせて使えますが、必ず最後に書くのがルールです。
label が最初の引数を受け取り、それ以降はすべて items にまとまって入ります。なお、Restパラメータを末尾以外の位置に書くと構文エラーになります。
Restパラメータと昔ながらの arguments オブジェクト
古いJavaScriptのコードでは、通常の関数内で arguments という特別な変数がよく使われていました。見た目は配列っぽいのですが実際は配列ではないため、配列メソッドをそのまま呼び出すことはできません。Restパラメータを使えば、この arguments をすっきり置き換えられます。
アロー関数にはそもそも arguments オブジェクトが存在しないので、可変長引数を受け取りたいときはRestパラメータを使うしかありません。新しく書くコードでは ...args を積極的に使っていきましょう。
関数呼び出しでのスプレッド構文
スプレッド構文は、Restパラメータのちょうど逆の働きをします。配列を展開して、呼び出し側で個々の引数としてバラバラに渡してくれるんです。
Math.max は配列ではなく、個々の数値を受け取る関数です。スプレッド構文が登場する前は Math.max.apply(null, nums) と書くしかありませんでしたが、今なら ... を付けるだけで済みます。
ここで注目してほしいのは、まったく同じ ... でも、関数の_定義_側ならRestパラメータ、関数の_呼び出し_側ならスプレッド構文になるという点です。つまり、書く位置でどちらの意味になるかが決まります。
配列リテラルでのスプレッド構文
配列リテラルの中でスプレッド構文を使うと、配列をコピーしたり結合したりできます。
[...a] を使うと、同じ要素を持つ新しい配列が手に入ります。元の配列をいじらずに並べ替えたり変更したいときに便利です。
scores はそのまま残っています。.sort がコピーの方に対して走ったからですね。ちょっとした習慣ですが、思わぬ副作用を起こしたくないコードを書くときに効いてきます。
オブジェクトリテラルでのスプレッド構文
スプレッド構文はプレーンオブジェクトにも使えて、プロパティをまとめて新しいオブジェクトに展開できます。
後ろにあるキーが勝ちます。updates.age が user.age を上書きし、city はそのまま引き継がれます。スプレッドを書く順序が結果を決めるので、デフォルト値と上書き値を重ねるときはこの点を意識しておきましょう。
デフォルトを先に、ユーザーの指定を後に書きます。こうすると fontSize はユーザー側が優先され、theme は既定値を引き継ぐ形になります。
浅いコピーの落とし穴
スプレッド構文によるコピーは 1階層だけ しか複製しません。ネストしたオブジェクトや配列は、元のデータとコピー先で参照が共有されたままです。
どちらの配列にも新しいタグが現れているのは、copy.tags と original.tags が同じ配列を参照しているからです。スプレッド構文はネストされた配列までは複製してくれず、参照をコピーしただけなんですね。
プレーンなデータを本当の意味でディープコピーしたいときは、structuredClone の出番です。
これで 2 つの配列は独立して扱えるようになりました。structuredClone はモダンブラウザと Node に標準で用意されていて、ネストした構造もまるごとコピーできます。浅いコピーでは足りない場面では、これを使うのが正解です。
分割代入における Rest
Rest は分割代入でも使えて、残りの要素やプロパティをまとめて受け取れます。
フィールドをいくつか取り出し、残りをまるごと1つのオブジェクトにまとめる――これはpropsを受け渡したり、機密フィールドを除外したり、データの一部だけを更新したバージョンを作ったりするときによく登場するパターンです。
password は取り出された上で捨てられ、残りはすべて safe に入ります。元のオブジェクトを書き換えることも、手動でコピーする必要もありません。
ここまでのまとめ
- 仮引数や分割代入パターンの中の
...nameは rest(残余)で、値を集めます。 - 関数呼び出し・配列リテラル・オブジェクトリテラルの中の
...valueは spread(展開)で、値を展開します。 - スプレッドによるコピーは 浅いコピー(シャローコピー) です。ネストされた構造は参照が共有されたままになるので、ディープコピーしたいときは
structuredCloneを使いましょう。 - Restパラメータは本物の配列です。
argumentsの代わりにこちらを使うのがおすすめです。 - オブジェクトリテラルでは後から書いたスプレッドが前のものを上書きします。デフォルト値+上書き、というパターンはこの仕組みで実現できます。
次回:クロージャ
JavaScriptの関数は、引数を受け取って値を返すだけの存在ではありません。定義されたときのスコープも覚えているんです。この「記憶」こそがクロージャで、コールバック・ファクトリ関数・次のページで出てくる多くのパターンを支える仕組みになっています。
よくある質問
RestパラメータとスプレッドはJavaScriptでどう違うの?
書き方はどちらも ... で同じですが、役割は真逆です。Restは複数の値を「集めて」1つの配列にまとめる使い方で、関数の仮引数や分割代入で登場します。一方スプレッドは、iterable やオブジェクトを「展開して」バラすもので、関数呼び出しや配列リテラル・オブジェクトリテラルの中で使います。受け取る側にあるのが Rest、渡す側にあるのがスプレッドと覚えておくと迷いません。
Restパラメータは関数の中でどう動くの?
たとえば function sum(...nums) と書くと、渡されたすべての引数が nums という本物の配列にまとめられます。ルールとして、Restパラメータは必ず仮引数リストの最後に置く必要があります。昔ながらの arguments オブジェクトと違って正真正銘の配列なので、.map や .filter、.reduce をそのまま呼び出せるのが嬉しいポイントです。
スプレッド演算子でディープコピーはできる?
できません。スプレッドがコピーするのは一番外側の階層だけ、いわゆるシャローコピーです。{ ...user } は新しいオブジェクトを作ってくれますが、中にネストされたオブジェクトや配列は元の参照を共有したままです。ディープコピーが欲しい場合は structuredClone(value) を使うか、プレーンなデータなら JSON 経由でシリアライズするのが定番です。