こんにちは!フロントエンドからNode.jsの深層まで、日夜JavaScriptと向き合っているシニアアーキテクトです。
今回は、JavaScriptの基本中の基本でありながら、実はエンジンの内部挙動と密接につながっている「`const`とオブジェクトの不変性、そしてV8の隠しクラス(Hidden Classes)」という非常に奥深いテーマについてお話しします。
「`const`で宣言したはずなのに、オブジェクトの中身が書き換えられてしまう?」
「それって本当に安全なの?」
他のプログラミング言語(JavaやC#など)からJavaScriptに入ってきた方ほど、この挙動に戸惑った経験があるのではないでしょうか。ここをクリアすれば、あなたのJavaScriptの理解度は一段と深まり、メモリ効率まで意識したワンランク上のコードが書けるようになりますよ。
それでは、一緒に本質の世界へ足を踏み入れてみましょう!
—
1. `const`は何を「定数」にしているのか?
まずは基本のおさらいからですね。JavaScriptの`const`は、変数そのものを「再代入不可能な定数」にするキーワードです。しかし、ここで大きな勘違いをしがちです。
`const`が固定しているのは、メモリ上の「住所(参照)」であって、その住所にある「建物の中身(プロパティ)」ではありません。
イメージ図で考えてみましょう。
[ const user = { name: “Alice” } ]
│
└─> メモリ上の住所 (0x001F) ──> 【 建物の中身: name = “Alice” 】
ここで、`user`という変数の箱に別のオブジェクトを入れようとすると(再代入)、`const`が「それはダメです!」とエラーを吐きます。
const user = { name: “Alice” };
// これはエラーになります(TypeError)
// 別の住所(オブジェクト)を代入しようとしたため
user = { name: “Bob” };
しかし、住所そのものは変えず、住所の先にある建物のペンキを塗り替えたり(プロパティの変更)、家具を新しく買い足したり(プロパティの追加)することは、`const`のガードをすり抜けて普通にできてしまいます。
const user = { name: “Alice” };
// 中身(プロパティ)の変更はできてしまう!
user.name = “Bob”; // 名前を書き換える
user.age = 25; // 新しいプロパティを追加する
console.log(user); // 出力: { name: ‘Bob’, age: 25 }
「あれ?`const`ってイミュータブル(不変)にするためのものじゃないの?」と思ったそこのあなた。そうなんです、`const`単体では「オブジェクトの構造や中身の変更」までは防げないのですね。
—
2. なぜオブジェクトの変更ができてしまうのか?(参照と値の仕組み)
JavaScriptの変数には、プリミティブ型(数値や文字列など)とオブジェクト型(オブジェクトや配列)で、メモリの扱い方に決定的な違いがあります。
- プリミティブ型:変数の箱の中に「値そのもの」が入る
- オブジェクト型:変数の箱の中には「メモリ上の居場所(参照)」が入る
`const`が縛っているのは、あくまで「変数の箱の中身(=参照)」です。そのため、箱の中に入っている住所が変わらなければ、`const`のルール違反にはならないというわけです。
もしオブジェクト自体を完全に不変(イミュータブル)にしたい場合は、`const`ではなく `Object.freeze()` という別の仕組みを使う必要があります。
const frozenUser = Object.freeze({ name: “Alice” });
// 厳格モード(strict mode)ではエラーになる、
// もしくはサイレントに無視されて変更されない
frozenUser.name = “Bob”;
console.log(frozenUser.name); // 出力: “Alice” のまま!
—
3. V8エンジンの裏側:プロパティの変更が「隠しクラス(Hidden Classes)」に与える影響
さて、ここからが本題であり、チーフアーキテクトとして最も伝えたかった「ランタイムの挙動」のお話です。
私たちが普段何気なく書いているJavaScriptコードは、Google ChromeやNode.jsの心臓部であるV8エンジンによって実行されています。JavaやC++のような静的型付き言語と違い、JavaScriptには「クラス定義」がなくても動的にオブジェクトを作ることができますよね。
「じゃあ、V8エンジンはどうやって高速にプロパティを探しているの?」
ここで登場するのが「隠しクラス(Hidden Classes / Shapes)」というV8の最適化メカニズムです。
隠しクラスとは何か?
V8は、動的なJavaScriptオブジェクトから効率的にプロパティを取り出すために、内部で「C++の構造体のようなもの(隠しクラス)」を裏で動的に生成しています。
例えば、以下のようなコードを考えてみましょう。
const obj1 = {}; // 空のオブジェクト
obj1.x = 10; // プロパティ x を追加
obj1.y = 20; // プロパティ y を追加
この時、V8の内部では以下のような「隠しクラスの遷移(Transition)」が起きています。
1. 初期状態(C0):何もプロパティがない状態
2. `x`の追加(C1):`x`というプロパティを持つ隠しクラスへ移行
3. `y`の追加(C2):`x`と`y`を持つ隠しクラスへ移行
この隠しクラスのおかげで、V8は「メモリの何番地を見に行けば`x`や`y`の値を取り出せるか」を高速に特定し、C言語並みのスピードでプロパティアクセスを行えるようにしています(これをインラインキャッシュと呼びます)。
⚠️ 注意すべきアンチパターン:プロパティの追加順序のバラツキ
ここで、先ほどの「`const`で宣言したオブジェクトのプロパティを後から自由に変更できる」という仕様が、V8エンジンにどのようなプレッシャーを与えるかが見えてきます。
もし、アプリケーションのあちこちで、オブジェクトのプロパティをバラバラの順序で追加したり削除したりするとどうなるでしょうか?
// パターンA
const point1 = {};
point1.x = 10;
point1.y = 20;
// パターンB(追加の順序が逆!)
const point2 = {};
point2.y = 20;
point2.x = 10;
人間から見れば`point1`も`point2`も「`x`と`y`を持つオブジェクト」として全く同じですが、V8の内部では全く異なる隠しクラスが割り当てられます。
その結果、V8の最適化エンジン(JITコンパイラ)が「あれ、この関数に渡されるオブジェクトの形がバラバラすぎて、メモリの構造が予測できないぞ…?」と混乱し、高速化の仕組み(メガモーフィックな状態)が外れてしまいます。これが、知らぬ間にアプリケーションのパフォーマンスをじわじわと低下させる原因の一つになるのです。
—
4. 現場で役立つベストプラクティス
ここまでの知見を踏まえて、日々のコーディングで意識すべきポイントをまとめました。
1. オブジェクトの「形」は最初(生成時)に決める
オブジェクトを生成する際は、後からプロパティを後付けするのではなく、必要なプロパティ(初期値が未定なら`null`や`undefined`など)を最初から揃えてオブジェクトリテラルで定義しましょう。これにより、V8が綺麗な隠しクラスのツリーを維持しやすくなります。
2. `const`の役割を正しく理解する
`const`は「再代入の抑止(コードの意図を明確にするため)」として使い、オブジェクトの中身の書き換えを防ぎたい場合は、データの性質に応じて設計レベルで不変性を担保するか、必要に応じて`Object.freeze()`を活用しましょう。
3. 動的なプロパティ追加・削除を乱用しない
`delete`演算子でプロパティを削除したり、後から動的に形を変えまくるコードは、V8の隠しクラスの最適化を破壊する特等席です。パフォーマンスが求められるホットパス(頻繁に実行されるループ内など)では特に注意してください。
—
まとめ
いかがでしたでしょうか?
一見シンプルな`const`という変数宣言も、メモリ管理やV8エンジンの「隠しクラス」というランタイムの挙動まで視座を広げてみると、JavaScriptという言語の深みと、高速に動かすためのエンジニアリングの妙が見えてきますよね。
「なぜこの書き方が推奨されるのか?」というエンジンの息吹を感じながらコードを書けるようになると、デバッグの精度もパフォーマンスチューニングの引き出しも劇的に広がります。
ここをクリアしたあなたなら、もう中級者の壁を確実に突破していますよ。ぜひ、今日のコードから意識してみてくださいね!