こんにちは!フロントエンドからNode.jsの内部挙動まで、JavaScriptの深淵を日々探求しているチーフアーキテクトです。
今回は、JavaScriptの基本中の基本でありながら、実はエンジニアの技量がモロに問われる「変数の宣言(`const`)」と、その背後にある「V8エンジンのメモリ最適化(隠しクラス)」の秘密についてお話ししていきます。
「`const`で宣言したオブジェクトのプロパティは書き換えられるのに、なぜ『不変(イミュータブル)』と言われるの?」
「そもそも、それがブラウザやNode.jsのパフォーマンスにどう影響するの?」
そんな疑問を持ったことはありませんか?
ここをクリアできれば、あなたの書くJavaScriptコードは、ただ動くだけのコードから、マシンリソースを極限まで引き出す「研ぎ澄ませたコード」に生まれ変わります。一緒に一歩ずつ、本質を紐解いていきましょう!
—
1. 初学者が勘違いしやすい「`const`の不変性」の正体
JavaScriptを学び始めると、「`const`で宣言した変数は、値を変えられない(定数になる)」と習いますよね。
例えば、こんなコードを書くとエラーになります。
const taxRate = 0.1;
taxRate = 0.15; // ❌ TypeError: Assignment to constant variable.
これは分かりやすいですね。「一度決めたら再代入できない」というルールです。
しかし、対象がオブジェクトになると、話が少し変わってきます。ここが多くの人が混乱するポイントです。
const user = {
name: ‘Taro’,
age: 25
};
// あれ? プロパティの書き換えができちゃった!?
user.age = 26;
console.log(user.age); // 26 出力される
「あれ? `const`なのに中身が書き換わっちゃったよ? バグなの?」と思ったそこのあなた、落ち着いてください。これはJavaScriptの仕様として正しい挙動です。
メモリの視点で見破る「変数」と「オブジェクト」の正体
なぜこのようなことが起きるのか? それは、JavaScriptがメモリ上でデータをどう扱っているかを知ると一目瞭然です。
イメージしてみてください。メモリという広大な倉庫の中に、小さな「箱(変数)」が置いてあります。
- `const taxRate = 0.1;` の場合、箱の中に直接 `0.1` という値が入っています。
- 一方、`const user = { … }` の場合、箱の中に入っているのはオブジェクトそのものではなく、「オブジェクトがメモリ上のどこに置いてあるかを示す住所(参照アドレス)」なんです。
つまり、`const` が固定しているのは「箱の中身(住所)」であって、「住所の先にある家(オブジェクトの中身)」ではないのです。
だからこそ、住所(参照)を別のものに書き換えようとするとエラーになりますが、
// ❌ 箱の中身(住所)を丸ごと書き換えようとするのでエラー
user = { name: ‘Jiro’, age: 30 };
住所の先にある家をリフォームしてペンキを塗り替える(プロパティを変更する)分には、`const` は何も文句を言いません。これが「参照の固定」と呼ばれる所以です。
—
2. V8エンジンの裏側:なぜ「隠しクラス(Hidden Classes)」が重要なのか?
さて、ここからが少し高度でエキサイティングなお話です。
JavaScriptは「動的言語」と呼ばれる言語で、C++やJavaのように「最初に型の構造を厳密に決めなくても、後から自由にプロパティを追加・変更できる」という柔軟性を持っています。
しかし、実行速度を極限まで高めたいJavaScriptエンジン(Google ChromeやNode.jsで使われている V8 など)にとって、この「何でもアリな柔軟性」は、実は最大の足枷(あしかせ)になります。
「毎回オブジェクトの中身がどうなっているか探すのは、CPUにとって非効率すぎる……!」
そこでV8エンジンは、「隠しクラス(Hidden Classes)」という裏技的な最適化メカニズムを使っています。
隠しクラスのイメージ図解
V8は、同じ構造(プロパティの並び順)を持つオブジェクト同士をグループ化し、内部で「設計図(隠しクラス)」を共有させます。
[初期状態のオブジェクト]
{ name: ‘Taro’, age: 25 }
↓ V8が自動生成
【隠しクラス A】(「name」の次に「age」がある構造)が割り当てられる 🚀 高速!
この隠しクラスが綺麗に機能しているとき、V8はメモリ上のデータをC++の構造体のようにダイレクトに読み書きできるため、爆速でコードが実行されます。
—
3. プロパティの動的変更が「最適化のエンジン」をブチ壊す瞬間
ここで、冒頭の `const` の話と繋がってきます。
`const` でオブジェクトを宣言したあと、私たちはうっかりこんなコードを書いてしまいがちです。
const settings = {}; // 空のオブジェクトとして宣言
// 後からどんどんプロパティを追加していく
settings.theme = ‘dark’;
settings.notifications = true;
settings.version = ‘1.2.0’;
一見、何の問題もない綺麗なコードに見えますよね。しかし、V8エンジンの内部メモリの視点から見ると、これは「最悪のアンチパターン」になり得ます。
なぜなら、空のオブジェクトからスタートして後からプロパティを追加するたびに、V8は「隠しクラスを何度も作り直す(ダウングレードや遷移を繰り返す)」という重い処理を強いられるからです。
1. 空のオブジェクト生成 = 【隠しクラス 0】
2. `theme` 追加 = 【隠しクラス 1】へ移行
3. `notifications` 追加 = 【隠しクラス 2】へ移行
4. `version` 追加 = 【隠しクラス 3】へ移行
これでは、せっかくのV8の最適化エンジンが台無しになってしまいます。大量のオブジェクトを生成するループ処理などでこれをやってしまうと、メモリ効率が著しく悪化し、ガベージコレクション(GC)の頻度が増えてアプリの動作がカクつく原因になります。
—
4. パフォーマンスを極限まで高める設計:イミュータブルとオブジェクトの形
では、私たちはどうコードを設計すべきなのでしょうか?
答えはシンプルです。「オブジェクトの構造(シェイプ)を最初から最後まで変えないこと」。
悪い例:後からプロパティを生やす
// ❌ 構造が流動的で、隠しクラスの最適化を殺してしまう
const createUser = (name, age) => {
const user = {};
user.name = name;
user.age = age;
return user;
};
良い例:最初から完全な形(シェイプ)で定義する
// 〇 最初からプロパティの順序と存在を固定して生成する
const createUser = (name, age) => {
return {
name: name,
age: age,
// 後から追加するかもしれないプロパティも、初期値としてnullやundefinedを入れておく
role: ‘guest’
};
};
このように、「`const`で参照を固定しつつ、オブジェクトのプロパティの構造(隠しクラス)もイミュータブル(不変)に保つ」という意識を持つだけで、V8エンジンはあなたのコードを愛し、最大限のパフォーマンスで応えてくれます。
さらにモダンな開発では、TypeScriptを活用して構造の型安全性を担保したり、`Object.freeze()` を使って誤ったプロパティの変更を防いだりするアプローチも非常に効果的です。
—
まとめ
- `const` は「値」ではなく「参照(住所)」を固定するもの。そのためオブジェクトの中身の変更は文法上可能。
- しかし、後からプロパティを自由に追加・変更すると、V8エンジンの「隠しクラス」の最適化が崩れ、パフォーマンスが低下する。
- プロフェッショナルなエンジニアは、文法上の制約だけでなく、ランタイム(V8)のメモリレイアウトまで意識してオブジェクトを設計する。
ここをクリアできれば、あなたのJavaScriptの基礎力は、ただの「動かせるレベル」から「大規模アプリや高負荷な処理も任せられるプロフェッショナルなレベル」へと確実にランクアップしています。
明日からのコードレビューや設計で、ぜひこの「隠しクラスとメモリの意識」を取り入れてみてくださいね。それではまた次回の知見でお会いしましょう!