JavaScript

JavaScriptの「ドット3つ(…)」って結局なんなんだ問題に決着をつける

2026年9月12日

他人のコードを読んでいると、... というドット3つの記号がやたらと出てくる場面、ありませんか。配列の中にあったり、関数の引数のところにあったり、オブジェクトのコピーで使われていたり。「同じ記号なのに場所によって意味が違う気がする……」と、駆け出しの頃の自分もかなり混乱しました。今回はこの...の正体を、実務でよく踏む地雷ポイントも交えつつ整理します。

「ドット3つ」は1つの記号で2つの顔を持っている

結論から言うと、...には「スプレッド構文」と「レスト(残余)パラメータ」という2つの役割があります。ただし記号自体は同じものなので、見分け方は「書く場所」しかありません。

配列やオブジェクトを展開する側(右辺やカッコの中)にあればスプレッド構文、関数の引数を受け取る側にあればレストパラメータです。個人的にはこの違いに気づくまで、「なんかまとめてる記号」くらいの雑な理解で済ませていた時期がありました。ちゃんと使い分けを覚えると、コードの見通しがかなり良くなります。

スプレッド構文:配列やオブジェクトをバラして展開する

まずはスプレッド構文から。これは配列やオブジェクトの中身を、括弧を外した状態でバラバラに展開してくれる機能です。

const fruits = ['apple', 'banana'];
const moreFruits = [...fruits, 'orange', 'grape'];

console.log(moreFruits);
// ['apple', 'banana', 'orange', 'grape']

...fruitsの部分が、配列の中身をそのまま展開してくれています。これがないとfruits.concat(['orange', 'grape'])のように書く必要があり、正直ちょっと面倒でした。ES6以降はこの書き方が主流になっています。

オブジェクトでも同じ考え方が使えます。

const user = { name: 'テスト太郎', age: 30 };
const updatedUser = { ...user, age: 31 };

console.log(updatedUser);
// { name: 'テスト太郎', age: 31 }

Reactを触ったことのある方なら、setStateuseStateの更新処理でこのパターンを見たことがあるはずです。元のオブジェクトを直接書き換えず、新しいオブジェクトとして複製しつつ一部だけ上書きする、というイミュータブルな考え方に非常に相性が良いんですよね。

配列のコピーに使うときの注意点(浅いコピーであること)

スプレッド構文は配列やオブジェクトの複製にもよく使われますが、これは**シャローコピー(浅いコピー)**である点は押さえておいたほうがいいです。

const original = { name: 'テスト', detail: { age: 20 } };
const copy = { ...original };

copy.detail.age = 99;

console.log(original.detail.age);
// 99 になってしまう

トップレベルのプロパティは複製されますが、ネストしたオブジェクトの参照はコピー元と共有されたままです。これで「コピーしたはずなのに元データまで書き換わった」という不具合を一度踏んだことがあります。ネストが深いデータをちゃんと複製したい場合はstructuredCloneを使うか、ライブラリに頼るのが安全です。

関数呼び出しでの展開

配列を関数の引数としてバラバラに渡したいときにも便利です。

function add(a, b, c) {
  return a + b + c;
}

const nums = [1, 2, 3];
console.log(add(...nums));
// 6

昔はFunction.prototype.applyを使ってこの手の処理を書いていましたが、スプレッド構文のおかげでかなりシンプルに書けるようになりました。Math.max(...arr)のように配列の最大値を求める場面でもよく使われます。

レストパラメータ:バラバラの値を1つの配列にまとめる

スプレッド構文とは逆に、レストパラメータは複数の値を1つの配列にまとめて受け取るための書き方です。関数の引数で使います。

function sum(...numbers) {
  return numbers.reduce((total, n) => total + n, 0);
}

console.log(sum(1, 2, 3, 4));
// 10

...numbersの部分に、渡された引数がすべて配列として詰め込まれます。何個引数が来るか分からない、いわゆる可変長引数を扱いたいときの定番パターンです。

レストパラメータには1つだけルールがあって、必ず引数リストの最後に書く必要があります。

// これはOK
function greet(greeting, ...names) {
  console.log(`${greeting}, ${names.join(' and ')}`);
}

// これはNG(SyntaxError)
function greetBad(...names, greeting) {
  // ...
}

レストパラメータの後ろに通常の引数を置くことはできません。地味にやりがちなミスなので気をつけてください。

オブジェクトの分割代入でも使える

配列だけでなく、オブジェクトの分割代入でも「残りのプロパティをまとめて受け取る」用途で使えます。

const user = { name: 'テスト太郎', age: 30, job: 'エンジニア' };
const { name, ...rest } = user;

console.log(name);
// 'テスト太郎'
console.log(rest);
// { age: 30, job: 'エンジニア' }

特定のプロパティだけ取り出して、残りをまるっと別オブジェクトにまとめたいときに重宝します。フォームの入力値から特定のキーだけ除外して送信したい、みたいなケースでちょくちょく使っています。

argumentsオブジェクトとの違い

レストパラメータが登場する前は、関数内で可変長の引数を扱うためにargumentsという特殊なオブジェクトが使われていました。

function oldSum() {
  console.log(arguments);
}

oldSum(1, 2, 3);
// Arguments(3) [1, 2, 3]

一見似ていますが、argumentsは配列のように見えて実際は配列ではない(Array-likeオブジェクト)ため、.map().filter()のような配列メソッドがそのままでは使えません。加えてアロー関数の中ではarguments自体が使えないという制約もあります。

const oldSumArrow = () => {
  console.log(arguments);
  // ReferenceError: arguments is not defined
};

この点、レストパラメータは正真正銘の配列として渡されるので、そのまま配列メソッドを使い倒せますし、アロー関数でも問題なく動作します。今から新しく書くコードであれば、argumentsを使う理由はほぼないと言っていいでしょう。

見分け方に迷ったら「渡すか、受け取るか」で考える

結局のところ、...を見たときに迷わない一番のコツは、「これは値を渡す側の記述か、受け取る側の記述か」を意識することです。

  • 配列リテラル[...]やオブジェクトリテラル{...}、関数呼び出しの引数の中に書かれていれば → スプレッド構文(展開)
  • 関数定義の引数リストや分割代入の左辺に書かれていれば → レストパラメータ(集約)

この2つは矢印が逆向きなだけで、コンセプトとしては表裏一体です。私は最初「展開のドット」「集約のドット」と勝手に呼んで覚えていました。案外これくらい雑な語呂合わせのほうが記憶には残ります。

まとめ

...というたった3文字の記号ですが、書かれている場所によって「展開する(スプレッド構文)」のか「まとめる(レストパラメータ)」のかがきっちり分かれているのが分かっていただけたかと思います。

配列やオブジェクトのコピー、関数への値渡し、可変長引数の受け取り、分割代入での残りプロパティの抽出など、実務でこの構文に触れる機会は本当に多いです。ES6以降のモダンなJavaScriptを書くうえでは避けて通れない存在なので、React含め、ライブラリのソースを読むときにも必ずどこかで出会うことになります。

個人的にハマりやすいポイントとしては、やはりスプレッド構文が浅いコピーにしかならない点です。ネストしたオブジェクトを複製しているつもりが実は参照を共有したままだった、という不具合は今でもたまに見かけます。「コピーしたから安全」と思い込まず、ネストの有無は常に意識しておくといいでしょう。

また、レストパラメータは引数リストの最後にしか置けないというルールも、慣れないうちはSyntaxErrorの原因になりがちです。エラーメッセージを見て「あれ、なんで怒られたんだろう」となったら、まずレストパラメータの位置を疑ってみてください。

どちらも一度パターンを覚えてしまえば迷うことはなくなる構文です。今回の整理が、...を見て身構えていた方の助けになれば幸いです。

余談ですが、TypeScriptを使っている場合はレストパラメータに型注釈をつける際、...numbers: number[]のように配列型で書く必要がある点も覚えておくと詰まりにくいです。可変長引数だからといってnumber型を単体で指定するとエラーになりますので、最初は戸惑うポイントかもしれません。

また、スプレッド構文とレストパラメータを同じ関数の中で併用するケースも実務では珍しくありません。たとえば受け取った引数の一部だけを個別に扱い、残りをそのまま別の関数に展開して渡す、といった処理です。書き方はシンプルなのに表現力が高いので、慣れてくると手放せなくなる構文だと思います。

最後に、今回紹介した内容はES6(ES2015)以降のモダンな環境が前提です。古いブラウザやNode.jsのバージョンをサポートする必要がある案件では、Babelなどのトランスパイラを噛ませるか、対象環境のサポート状況を事前に確認しておくと安心です。とはいえ現在主要なブラウザはすべて対応済みなので、通常のフロントエンド開発であればほとんど気にする必要はないでしょう。


【参考文献】

  • MDN Web Docs – スプレッド構文: https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Spread_syntax
  • MDN Web Docs – レストパラメータ: https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Functions/rest_parameters
  • MDN Web Docs – arguments オブジェクト: https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Functions/arguments

Related Posts