「不変データ」とは何か(まずここを押さえる)
不変データ(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配列を「不変に」扱うスプレッド構文
末尾・先頭に要素を足した新配列を作る
push や unshift は元配列を変更します。不変にしたいときは、スプレッドで「元+追加」の配列を作ります。
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+ を使ううえで非常に大きな武器になります。
