JavaScript | ES6+ 文法:スプレッド構文 – 不変データ作成

JavaScript
スポンサーリンク

「不変データ」とは何か(まずここを押さえる)

不変データ(immutable data)とは、「一度作ったデータを書き換えず、変更したいときは新しいデータを作る」という考え方です。
ここが重要です:

  • 配列.push()obj.prop = ... のように、既存のデータを直接いじるのは「可変」
  • スプレッド構文(...)を使って「元+変更点」から新しい配列/オブジェクトを作るのが「不変」

JavaScript 自体は「不変」を強制しませんが、ES6+ の書き方と相性がよく、バグの少ないコードを書くための重要なスタイルです。

// 可変(元を直接変更)
const arr1 = [1, 2];
arr1.push(3); // arr1 自体が変わる

// 不変(新しい配列を作る)
const arr2 = [1, 2];
const arr3 = [...arr2, 3]; // arr2 はそのまま、arr3 が新バージョン
JavaScript

配列を「不変に」扱うスプレッド構文

末尾・先頭に要素を足した新配列を作る

pushunshift は元配列を変更します。不変にしたいときは、スプレッドで「元+追加」の配列を作ります。

const base = [2, 3];

// 先頭に 1 を付けた新配列
const withHead = [1, ...base];

// 末尾に 4 を付けた新配列
const withTail = [...base, 4];

console.log(base);     // [2, 3]
console.log(withHead); // [1, 2, 3]
console.log(withTail); // [2, 3, 4]
JavaScript

ここが重要です:
「元の配列は一切変えない」、変更が必要なら「新しい配列を作る」
というルールを守ることで、「いつの間にか元の配列が変わっている」事故を防げます。

真ん中の要素を差し替えた新配列を作る

インデックスで一部だけ変えたいときも、「前後を slice + スプレッド」でつなぎ直します。

const original = [1, 2, 3, 4];

// インデックス2(値3)だけ 99 に変えた新配列
const updated = [
  ...original.slice(0, 2), // [1, 2]
  99,
  ...original.slice(3)     // [4]
];

console.log(original); // [1, 2, 3, 4]
console.log(updated);  // [1, 2, 99, 4]
JavaScript

「一部を変えたい=新しい配列を組み立て直す」という感覚です。

破壊的メソッド(sort / reverse)を安全に使う

sort()reverse() は元配列を破壊します。スプレッドでコピーしてから使えば、不変スタイルを保てます。

const scores = [72, 88, 95, 64];

// 元を壊さないでソート
const sorted = [...scores].sort((a, b) => a - b);

console.log(scores); // [72, 88, 95, 64]
console.log(sorted); // [64, 72, 88, 95]
JavaScript

ここが重要です:
「破壊的メソッドを使う=先に [...元] でコピー」をセットで覚えてください。

オブジェクトを「不変に」扱うスプレッド構文

一部のプロパティだけ変えた新オブジェクトを作る

設定や状態オブジェクトでよく使う形です。
元を壊さず、「一部だけ違うコピー」を作ります。

const config = {
  theme: "light",
  lang: "ja",
  debug: false
};

// debug だけ true にした新バージョン
const newConfig = {
  ...config,
  debug: true
};

console.log(config);    // { theme: "light", lang: "ja", debug: false }
console.log(newConfig); // { theme: "light", lang: "ja", debug: true }
JavaScript

ここが重要です:

  • { ...config } で「元と同じ中身のコピー」を作る
  • 後ろに debug: true と書くことで、そのプロパティだけ上書きされる
  • 元の config は一切変わらない

デフォルト設定+ユーザー設定を不変にマージする

不変スタイルでは、既存のオブジェクトを上書きせず、常に「マージ結果の新オブジェクト」を返します。

const defaultOptions = {
  method: "GET",
  timeout: 3000
};

function makeOptions(userOptions = {}) {
  return {
    ...defaultOptions,
    ...userOptions
  };
}

const opts = makeOptions({ timeout: 5000 });
console.log(defaultOptions); // そのまま
console.log(opts);           // timeout だけ 5000 に
JavaScript

ネストした状態の一部を不変に更新する

外側も内側も「スプレッド+上書き」の組み合わせで、新しい状態を作ります。

const state = {
  user: { name: "Alice", age: 20 },
  ui: { theme: "light", lang: "ja" }
};

// ui.theme だけ "dark" に変更した新 state
const newState = {
  ...state,
  ui: {
    ...state.ui,
    theme: "dark"
  }
};

console.log(state.ui.theme);    // "light"
console.log(newState.ui.theme); // "dark"
JavaScript

ここが重要です:
「外側をスプレッド → 変えたい内側もスプレッド+上書き」という二段構えで、不変な更新を作るのが定石です。

「不変」にすることで何が嬉しいのか(重要な考え方)

バグの原因を追いやすくなる

データが「いつ変わったか」が予測しにくいと、デバッグが非常に難しくなります。
不変スタイルでは、

  • 一度作ったオブジェクト/配列は、その後どこでも変更されない
  • 状態の変化は、「新しいオブジェクト/配列を作る瞬間」にしか起きない

ので、「どこで変わったのか」がコードから追いやすくなります。

関数が「安全な部品」になりやすい

不変スタイルでは、関数は「入力を受け取り、新しい値を返すだけ」の形にしやすくなります(純粋関数に近づく)。

function addItem(items, item) {
  // 元の items を触らず、新しい配列を返す
  return [...items, item];
}

const base = [1, 2];
const extended = addItem(base, 3);

console.log(base);     // [1, 2]
console.log(extended); // [1, 2, 3]
JavaScript

こういう関数は、どこから呼んでも「周りの状態を壊さない」ので、再利用性が高くなります。

React などのフレームワークと相性が良い

React や Redux など、多くのモダンなライブラリは「不変データ」を前提とした最適化や設計をしています。
「参照が変わったかどうか(=== で比較)」だけで「変化があったか」を判断できるからです。

スプレッド構文を使った不変スタイルは、こうしたライブラリと自然にマッチします。

浅いコピーと不変データ(ここも押さえておく)

スプレッド構文によるコピーは「浅いコピー」です。
外側の配列/オブジェクトは新しくなりますが、中のオブジェクトや配列は共有されます。

const state = {
  user: { name: "Alice" }
};

const copy = { ...state };
copy.user.name = "Bob";

console.log(state.user.name); // "Bob"(中身は共有)
JavaScript

ここが重要です:

  • 不変「っぽく」見えても、ネストした中身を書き換えると元にも影響する
  • 真の不変にしたいなら、「どこをコピーしてどこを共有するか」を意識する

現実的には、

  • 基本方針として「ネストを含む更新も、スプレッドで新しいオブジェクトを作る」
  • ネストの更に内側まで完全に独立させたいときは、そこもコピーを取る

という使い分けが多いです。

例題で理解を固める

// 1) 不変なリスト追加
function addTodo(todos, text) {
  return [...todos, { id: Date.now(), text, done: false }];
}

const todos1 = [];
const todos2 = addTodo(todos1, "勉強する");
console.log(todos1); // []
console.log(todos2); // 1件だけ入った新配列

// 2) 不変なフラグ更新
function toggleDebug(config) {
  return { ...config, debug: !config.debug };
}

const cfg1 = { debug: false };
const cfg2 = toggleDebug(cfg1);
console.log(cfg1.debug); // false
console.log(cfg2.debug); // true

// 3) 不変なネスト更新(ユーザー名を変更)
function renameUser(state, newName) {
  return {
    ...state,
    user: {
      ...state.user,
      name: newName
    }
  };
}

const state1 = { user: { name: "Alice", age: 20 }, count: 0 };
const state2 = renameUser(state1, "Bob");
console.log(state1.user.name); // "Alice"
console.log(state2.user.name); // "Bob"

// 4) 不変なソート
const scores = [3, 1, 2];
const sortedScores = [...scores].sort();
console.log(scores);        // [3, 1, 2]
console.log(sortedScores);  // [1, 2, 3]
JavaScript

まとめ

スプレッド構文を使った「不変データ作成」の核心は、
「元を絶対に直接書き換えず、“元+変更点”から新しい配列/オブジェクトを作る」ことです。

配列なら [...old, newItem][..., ...before, changed, ...after]
オブジェクトなら { ...old, changedProp: value }{ ...old, nested: { ...old.nested, ... } } の形で、
「新バージョン」を組み立てます。

このスタイルを徹底すると、

  • 「いつの間にか書き換わった」が起きにくくなり
  • 関数が安全な部品になり
  • React などのライブラリとも自然に噛み合う

ので、ES6+ を使ううえで非常に大きな武器になります。

タイトルとURLをコピーしました