こんにちは!フロントエンドからNode.jsの深部まで、JavaScriptの生態系をくまなく愛するチーフアーキテクトです。
今回は、JavaScriptを学び始めた誰もが一度は「えっ、どういうこと?」と立ち止まる、`const`の不変性(イミュータビリティ)とメモリの裏側について、徹底的に解き明かしていきましょう。
「`const`で宣言したのに、オブジェクトの中身が書き換えられちゃったんだけど……バグ?」
そんな疑問を持ったことはありませんか? ここをクリアすれば、JavaScriptのメモリ管理の基本はバッチリマスターできますよ。一緒にエンジニアとしての視座を一段引き上げてみましょう!
—
1. `const`の本当の意味を正しく知ろう
まずは基本のおさらいです。モダンなJavaScriptでは、変数を宣言するときに古い `var` ではなく、`let` や `const` を使うのが常識ですよね。
- `let`: 再代入ができる変数
- `const`: 再代入ができない定数
ここでポイントなのは、`const`が防いでいるのは「再代入(Re-assignment)」であって、データそのものが変化しないこと(イミュータブル)ではない、という点なんです。
まずはシンプルなプリミティブ型(数値や文字列など)の挙動を見てみましょう。
// 数値型を const で宣言
const score = 100;
// 再代入を試みる
score = 200; // ⚠️ ここで TypeError が発生します!
実行すると、JavaScriptエンジン(V8など)は「`const`で固定されたメモリ領域に新しい値を書き込もうとしたな!」と検知し、容赦なくエラーを投げます。これは分かりやすいですよね。
では、本題の「オブジェクト」を `const` で宣言したときはどうなるのでしょうか?
—
2. オブジェクトを `const` にしたときの「奇妙な挙動」
次のコードを見てください。
// ユーザー情報をオブジェクトとして const で宣言
const user = {
name: ‘Taro’,
age: 25
};
// おっと、年齢が変わりました。プロパティを書き換えてみます
user.age = 26;
// 名前のプロパティも追加しちゃおう
user.country = ‘Japan’;
console.log(user);
// 出力結果: { name: ‘Taro’, age: 26, country: ‘Japan’ }
あれっ? エラーになりませんね。それどころか、`const` で宣言したはずの `user` オブジェクトの中身が、見事に書き換わっています。
「`const` なのに書き換えられるなんて、名前詐欺じゃないか!」と思いましたか?
いいえ、JavaScriptのエンジンは嘘をついていません。この現象の謎を解くカギは、V8などのランタイムがメモリ上でデータをどう扱っているかという「メモリレイアウト」にあります。
—
3. スタックとヒープ:メモリ上の「住所」と「中身」の物語
JavaScriptがメモリを管理するとき、大まかに分けて「スタック(Stack)」と「ヒープ(Heap)」という2つの空間を使います。
- スタック領域: 変数名と、その「値(または値への参照)」が直感的に配置される軽量で高速なスペース。
- ヒープ領域: オブジェクトや配列など、サイズが可変で複雑なデータをドンと置いておく広大なスペース。
ここで、先ほどの `const user = { name: ‘Taro’, age: 25 };` というコードが実行されたとき、メモリの内部で何が起きているのかをイメージ図で覗いてみましょう。
【メモリのイメージ】
[スタック領域] [ヒープ領域]
変数名 値(ポインタ) アドレス 実際のオブジェクトデータ
————————- ———————————–
user ───> [ 0x7fff… ] ──────> 0x7fff… { name: ‘Taro’, age: 25 }
お気づきでしょうか?
スタック領域に保存されている `user` という変数が持っているのは、オブジェクトそのものではなく、ヒープ領域にあるオブジェクトの「住所(メモリ上の参照・ポインタ)」なのです。
`const` が守っているものの正体
`const` が死守しているのは、このスタック領域にある「`user` という変数が持つアドレス(住所)」です。
- `user = { name: ‘Jiro’ };` と別のオブジェクトを代入しようとする
👉 スタック上の住所を書き換えることになるため、`const` が「ダメです!」とエラーを出す。
- `user.age = 26;` とプロパティをいじる
👉 スタック上の住所(矢印の向き)は変えず、矢印の先にあるヒープ領域の家の中をリフォームしているだけなので、`const` は怒らない。
つまり、`const` は「住所変更お断り」の札であって、「家の中の家具を動かすな」という意味ではないということなんです。この違い、すごく腑に落ちますよね!
—
4. 陥りがちな罠と、本当の意味でイミュータブルにする方法
実務やデバッグの現場では、「オブジェクトの中身すら一切書き換えられたくない(完全なイミュータブルにしたい)」という場面に数多く遭遇します。意図しないところでデータが書き換わると、バグの温床になってしまいますからね。
そんなときは、JavaScriptの標準機能である `Object.freeze()` を使います。
// Object.freeze() でオブジェクトを凍結する
const frozenUser = Object.freeze({
name: ‘Hanako’,
age: 30
});
// プロパティの書き換えを試みる
frozenUser.age = 31;
console.log(frozenUser.age);
// 出力結果: 30 (書き換わらずに無視される、または strict モードではエラーになる)
`Object.freeze()` を使えば、ヒープ上のオブジェクトの改変をガッチリとガードしてくれます。ただし、これはネストした(入れ子になった)オブジェクトの深部までは凍結してくれない「浅い凍結(シャロー・フリーズ)」なので、複雑なデータを扱うときは注意が必要ですよ。
—
まとめ:メモリを脳内トレースできるようになれば無敵
今回は、`const` の不変性とメモリレイアウトの真実について解説しました。
- `const` が固定するのは、スタック上の「ポインタ(参照・住所)」である。
- オブジェクトのプロパティ変更は、ヒープ上のデータ書き換えなので `const` では防げない。
- 中身まで完全に固定したい場合は、`Object.freeze()` などのアプローチを検討する。
このメモリの仕組み(スタックとヒープの役割分担)が頭の中でスッとイメージできるようになると、JavaScriptの挙動で迷うことが劇的に減ります。
ここをクリアしたあなたなら、もう変数やスコープの扱いで戸惑うことはありません。自信を持って、次のモダンなコードを書きに行きましょう!