スプレッド構文

生田 陸人
ゆめさくエンジニア / 現役ソフトウェアエンジニア
編集 LuaGate編集部

このレッスンで分かること

  • ... は配列やオブジェクトを展開して並べ直す記号です
  • [...arr, x] でコピーに追加、{ ...a, ...b } でマージができます
  • コピーは浅いので、入れ子のオブジェクトは元と共有されたままになります

スプレッド構文 とは

... を使って配列やオブジェクトの中身をその場に展開する書き方です。元を壊さずにコピーや合成ができます。

元を壊さずに増やす

前々回、const の配列に push して中身が変わることを確かめました。あれは仕組みの確認としては大事でしたが、実際のコードで多用すると困ることが起きます。

function addWork(works, newWork) { works.push(newWork); return works; } const original = [{ title: "プロフィールサイト", year: 2026 }]; const updated = addWork(original, { title: "自己紹介カード", year: 2026 }); console.log(original.length); // 2 呼び出した側の配列まで増えている

updated を作ったつもりが original も一緒に変わってしまいました。呼び出し側から見れば「渡しただけなのに壊された」わけです。こうした書き換えは、規模が大きくなるほど原因の特定が難しいバグになります。

function addWork(works, newWork) { return [...works, newWork]; }

...works は「works の中身をこの場にばらまく」という意味です。新しい配列リテラル [] の中でばらまいて、その後ろに newWork を置く。結果として、元の配列はそのままで、要素が 1 つ増えた別の配列ができあがります。

元のデータを書き換えずに新しいデータを作る書き方を、イミュータブルな更新と呼びます。React をはじめとする現代のフレームワークは、この前提で「変化」を検知しているので、入門のうちからこちらに慣れておくと後がとても楽になります。

配列でのスプレッド

... は配列リテラルの中で自由に置けます。

const skills = ["HTML", "CSS"]; console.log([...skills]); // ["HTML", "CSS"] コピー console.log([...skills, "JavaScript"]); // ["HTML", "CSS", "JavaScript"] 末尾に追加 console.log(["基礎", ...skills]); // ["基礎", "HTML", "CSS"] 先頭に追加 console.log([...skills, ...skills]); // 4 要素 連結

concat でも同じことはできますが、... のほうが位置を自由に決められて読みやすいです。

やりたいことスプレッドでの書き方元の配列
コピー[...arr]変わらない
末尾に追加[...arr, x]変わらない
先頭に追加[x, ...arr]変わらない
連結[...a, ...b]どちらも変わらない
末尾に追加 (破壊的)arr.push(x)変わる

関数の引数に配列を展開して渡すこともできます。

const views = [120, 80, 40]; console.log(Math.max(...views)); // 120

Math.max は数値を並べて受け取る関数なので、配列のままでは渡せません。... でばらしてから渡します。

オブジェクトでのスプレッド

{} の中でも同じことができます。こちらはキーが同じ場合の上書きが要点です。

const base = { title: "無題", year: 0, views: 0, published: false }; const input = { title: "プロフィールサイト", year: 2026 }; const work = { ...base, ...input }; console.log(work); // { title: "プロフィールサイト", year: 2026, views: 0, published: false }

同じキーがあった場合、後に書いたほうが勝ちます。この性質を使うと「既定値を先に置き、実際の値で上書きする」という定石が書けます。逆にすると既定値で上書きされてしまうので、順番はとても重要です。

1 つのキーだけ変えた新しいオブジェクトを作るのも定番です。

const work = { title: "プロフィールサイト", year: 2026, views: 120 }; const updated = { ...work, views: 121 }; console.log(work.views); // 120 元はそのまま console.log(updated.views); // 121

{ ...work, views: 121 } は「work をコピーして views だけ差し替える」と読みます。この 1 行が書けるようになると、状態を扱うコードが一気に短くなります。

コピーは浅い

ここがこのレッスンでいちばん大事なところです。スプレッドで作られるコピーは浅いコピーです。1 階層目は新しく作られますが、値がオブジェクトや配列だった場合、その実体は元と共有されたままになります。

const work = { title: "プロフィールサイト", links: { url: "https://example.com/profile" }, }; const copy = { ...work }; copy.title = "改訂版"; console.log(work.title); // プロフィールサイト こちらは無事 copy.links.url = "https://example.com/other"; console.log(work.links.url); // https://example.com/other こちらは道連れ

copy.linkswork.links は、前のレッスンで学んだ「同じ場所を指す 2 枚の札」の関係です。前のレッスンで見た Object.freeze が浅かったのとまったく同じ理由になります。

配列でも同じです。

const works = [{ title: "プロフィールサイト" }]; const copied = [...works]; copied.push({ title: "自己紹介カード" }); console.log(works.length); // 1 配列そのものは別物 copied[0].title = "改訂版"; console.log(works[0].title); // 改訂版 中の要素は共有されている

深いところまでコピーしたいときは、階層ごとにスプレッドを重ねます。

const deepCopy = { ...work, links: { ...work.links } };

近年は structuredClone(work) という組み込み関数も使えますが、入門のうちは「浅い」という事実を知っておくことが何より大切です。

浅いコピーを深いコピーだと思い込むのは、経験者でもやる勘違いです。「一段だけ新しい、その先は元と同じ」と唱えながら書くと事故が減ります。

レスト構文にも触れておく

... は左辺に書くと逆の意味になります。展開ではなく、残りをまとめる働きです。

const [first, ...rest] = ["HTML", "CSS", "JavaScript"]; console.log(first); // HTML console.log(rest); // ["CSS", "JavaScript"] const { title, ...others } = { title: "プロフィールサイト", year: 2026, views: 120 }; console.log(others); // { year: 2026, views: 120 }

前のレッスンの分割代入と組み合わせて使います。「1 つだけ取り出して残りはまとめて持っておく」という場面で便利です。同じ ... でも、右辺なら展開、左辺ならまとめる、と覚えてください。

よくある間違い

  • マージの順番を逆にする
const work = { ...input, ...base }; // 既定値で実際の値が上書きされる const work2 = { ...base, ...input }; // 正しい。後ろが勝つ
  • 浅いコピーで安心する
const copy = { ...work }; copy.tags.push("JavaScript"); console.log(work.tags.length); // 元も増えている

copy.tags は元と同じ配列です。中身を触るなら { ...work, tags: [...work.tags] } のように階層ごとにコピーします。

  • オブジェクトを [] で展開しようとする
const arr = [...work]; // TypeError work is not iterable

[] の中で展開できるのは配列や文字列などの反復可能なものだけです。オブジェクトを展開したいなら {} の中で行います。

この章のポイント

ここまでの要点 右辺の ... は展開、左辺の ... はまとめる。オブジェクトのマージは後ろが勝つ。コピーは浅く、入れ子は元と共有される。

やってみよう

作品配列のコピーに新しい作品を足す関数を書きます。元の配列には触りません。

  1. 引数 works (作品オブジェクトの配列) と newWork (作品オブジェクト) を受け取る
  2. { views: 0, published: false } を既定値として newWork とマージする
  3. マージしたオブジェクトを works のコピーの末尾に足した新しい配列を返す
  4. push は使わない

newWorkviews が入っていればその値が使われ、入っていなければ 0 になります。既定値を先、newWork を後ろに置くのがポイントです。

要件

  1. 関数名は withNewWork、引数は worksnewWork の 2 つ
  2. { views: 0, published: false } を既定値として newWork とマージすること
  3. 戻り値は元の配列に手を加えずに作った新しい配列
  4. push を使わずスプレッド構文で書くこと

入出力例

withNewWork([], {"title":"プロフィールサイト","year":2026})[{"published":false,"title":"プロフィールサイト","views":0,"year":2026}] withNewWork([{"published":true,"title":"プロフィールサイト","views":120,"year":2026}], {"title":"自己紹介カード","views":80,"year":2026})[{"published":true,"title":"プロフィールサイト","views":120,"year":2026},{"published":false,"title":"自己紹介カード","views":80,"year":2026}] withNewWork([{"published":true,"title":"プロフィールサイト","views":120,"year":2026},{"published":false,"title":"自己紹介カード","views":80,"year":2026}], {"published":true,"title":"スキル一覧の表","year":2026})[{"published":true,"title":"プロフィールサイト","views":120,"year":2026},{"published":false,"title":"自己紹介カード","views":80,"year":2026},{"published":true,"title":"スキル一覧の表","views":0,"year":2026}] withNewWork([], {})[{"published":false,"views":0}]

ヒント

main.js
main.js
学習モード

メモ

スプレッド構文

⌘S で保存