JavaScript | 配列・オブジェクト:配列の基礎 – 配列の変更可能性

JavaScript JavaScript
スポンサーリンク

配列の変更可能性とは何か

配列は「参照型のオブジェクト」で、デフォルトでは中身を自由に変更できます。push・pop・splice・sort などのメソッドは配列を“その場で書き換える”ため、同じ配列を共有している変数や呼び出し元にも影響が伝わります。ここが重要です:const は“再代入を禁止”するだけで、配列の中身の変更(ミューテーション)までは止めません。


const とミューテーション(混同しやすいポイント)

const でも中身は変えられる

const a = [1, 2];
a.push(3);   // OK(中身を変更)
console.log(a); // [1, 2, 3]
// a = [9];  // NG(再代入はできない)
JavaScript

配列そのものの“場所”は固定ですが、その中身は変更できます。不変にしたいなら「非破壊の書き方」を選ぶか、後述の Object.freeze を使います。

参照共有の影響

const a = [1, 2];
const b = a;   // 同じ配列を指す
b.splice(0,1); // a も b も [2] になる
JavaScript

ここが重要です:代入は“参照の共有”。破壊的操作は共有者全員に波及します。


破壊的操作と非破壊的操作(どちらを選ぶべきか)

代表的な破壊的操作(元配列を書き換える)

const arr = [3, 1, 2];
arr.push(4);         // 末尾追加
arr.pop();           // 末尾削除
arr.splice(1, 1);    // 任意位置の挿入・削除
arr.sort();          // 並び替え(比較関数がないと文字列基準)
JavaScript

破壊的操作は短く書けますが、共有や状態管理(React/Vue 等)では不具合の原因になります。

安全な非破壊操作(新配列を返す)

const arr = [3, 1, 2];
const added    = [...arr, 4];          // 追加(スプレッド)
const removed  = arr.filter((_, i) => i !== 1); // 削除
const sorted   = [...arr].sort((a,b) => a - b); // ソートはコピーしてから
const doubled  = arr.map(x => x * 2);  // 変換
JavaScript

ここが重要です:非破壊は“元を壊さない”。予測可能性が高く、バグと副作用を減らせます。状態管理や共同編集で推奨されるスタイルです。


“不変にする”ための手段(禁止・凍結・型で縛る)

Object.freeze で完全凍結(浅い凍結)

const a = Object.freeze([1, 2, 3]);
a.push(4);      // TypeError(厳格モード)/失敗(非厳格)
a[0] = 9;       // 変更されない
JavaScript

freeze は「追加・削除・変更」を阻止します。ただし“浅い”凍結のため、内部のオブジェクトまでは止めません。

深い凍結の考え方

function deepFreeze(obj) {
  Object.getOwnPropertyNames(obj).forEach((key) => {
    const val = obj[key];
    if (val && typeof val === "object") deepFreeze(val);
  });
  return Object.freeze(obj);
}
JavaScript

入れ子まで凍結したい場合は再帰的に freeze します(巨大データではコストに注意)。

そもそも非破壊で書く(実務の第一選択)

不変を“仕組み”で強制するより、map/filter/spread を徹底して「入力は触らず、出力だけ新しくする」設計にすると、体験・保守・性能のバランスが良くなります。


コピーの本質(浅いコピーと深いコピー)

浅いコピー(一次元だけ独立)

const a = [{ id: 1 }, { id: 2 }];
const b = [...a];       // または a.slice()
b[0].id = 999;
console.log(a[0].id); // 999(内側オブジェクトは共有のまま)
JavaScript

浅いコピーは「配列の枠」を複製するだけで、内側の参照は共有されます。プリミティブ配列なら十分ですが、入れ子の更新では注意が必要です。

深いコピー(入れ子まで別物)

const a = [{ id: 1, tags: ["x"] }];
const b = structuredClone(a);   // 標準の深いコピー
b[0].tags.push("y");
console.log(a[0].tags); // ["x"](元は不変)
JavaScript

ここが重要です:入れ子の独立が必要なら深いコピー。JSON.parse(JSON.stringify(…)) は関数や undefined を失うため、structuredClone が推奨です。


実践パターン(更新・追加・削除を非破壊で)

置き換え(id 指定で1件更新)

const items = [{id:1},{id:2}];
const updated = items.map(x => x.id === 2 ? { ...x, name: "new" } : x);
JavaScript

“対象だけスプレッドで上書き”が安全で読みやすい定石です。

追加(末尾へ)

const added = [...items, { id: 3 }];
JavaScript

配列を新しく作るため、差分検知(再レンダリング)にも素直です。

削除(条件で除外)

const removed = items.filter(x => x.id !== 2);
JavaScript

filter は“残す基準”で書くので、削除の意図が明確になります。


よくある落とし穴と対策

const で“不変”だと誤解する

const は再代入不可なだけ。中身は変わるため、非破壊のメソッド選択で不変を担保してください。

sort/splice をうっかり使って状態を壊す

並び替えや挿入・削除は破壊的です。コピーしてから sort、削除は filter、挿入はスプレッド+スライスで非破壊に書き換えましょう。

参照共有を忘れて別名の変数が同時に変わる

代入は参照共有です。共有を避けたい箇所では必ずコピー(浅い/深い)を取ってから操作します。

浅いコピーで入れ子が壊れる

内側のオブジェクト・配列は共有のまま。入れ子の独立が必要なら structuredClone、または局所的にスプレッドで部分コピーして更新します。


まとめ

配列は“変更可能”が基本で、const でも中身の変更は可能です。副作用を避けたいなら、破壊的操作(push/splice/sort)より非破壊の手法(map/filter/スプレッド/コピー)を選ぶのが最善です。参照共有を前提に、必要な箇所では浅いコピーと深いコピーを使い分ける。より強く不変にしたい場面では Object.freeze(必要なら深い凍結)も検討する。これらを徹底すれば、初心者でも「配列の変更可能性」を味方につけ、予測可能で壊れにくいコードが書けます。

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