こんにちは!JavaScriptのコアな仕組みからモダンなフロントエンド開発まで、日夜コードと向き合っているフロントエンド・アーキテクトの私です。
今回は、JavaScriptの変数宣言において誰もが一度は誤解しがちな「`const`の本当の意味」と、オブジェクトを完全に守り抜くための「`Object.freeze`との組み合わせによる堅牢な設計パターン」について、V8エンジンのメモリ構造の裏側まで少しだけ覗きながら、優しく、そして深く解説していきますね。
ここをクリアすれば、あなたの書くコードの安全性は劇的に跳ね上がります。一緒にバッチリマスターしていきましょう!
—
1. `const`は「値の不変(イミュータブル)」ではなく「再代入の禁止」である
JavaScriptを学び始めて最初に覚える変数宣言といえば、`const`ですよね。「定数を宣言するキーワード」として教わった方が多いはずです。
例えば、こんなコードを書いたとします。
const taxRate = 0.1;
// taxRate = 0.08; // ❌ エラー!再代入はできません
これはバッチリ動きますし、意図しない再代入を防いでくれる最高の機能です。では、次のコードはどうでしょうか?
const user = {
name: ‘Taro’,
age: 25
};
// プロパティの書き換えは…?
user.age = 26; // ⚠️ えっ、エラーにならないの!?
console.log(user.age); // 出力: 26
「あれ?`const`を使っているのに、オブジェクトの中身が変わっちゃったぞ?」と戸惑いませんでしたか?そうなんです。これが、初学者が一番最初にハマるJavaScriptの罠であり、本質を理解するための重要な分岐点になります。
メモリの視点から見る `const` の正体
JavaScriptのエンジン(V8など)の頭の中を少し覗いてみましょう。
変数にオブジェクトを代入したとき、メモリ(ヒープ領域)の空間には「オブジェクトの実体」がポツンと作られます。そして、変数(`user`)が持っているのは、その実体が「どこにありますよ」という「メモリアドレス(参照)」でしかありません。
`const`が固めているのは、この「変数に格納されたメモリアドレスの箱」です。
[ const user ] ──(参照を固定!変更不可)──> { name: ‘Taro’, age: 25 }
│
(中身の書き換えは自由!)
↓
{ name: ‘Taro’, age: 26 }
つまり、`const`は「別のオブジェクトの住所に書き換えること(再代入)」は厳禁しますが、「住所の先にある家の中の家具を勝手に模様替えすること(プロパティの変更)」までは止めてくれないのです。
—
2. オブジェクトを完全に凍結する:`Object.freeze()` の登場
「じゃあ、オブジェクトの中身すら一切書き換えられたくない(完全な不変性にしたい)場合はどうすればいいの?」という疑問が湧きますよね。
そこで登場するのが、JavaScriptが標準で持っている魔術、`Object.freeze()` です。
`Object.freeze()`をオブジェクトに通すと、そのオブジェクトは「凍結(フリーズ)」され、プロパティの追加、削除、そして値の変更が一切できなくなります。
実際のコードでその挙動を見てみましょう。
// const と Object.freeze を組み合わせる
const immutableUser = Object.freeze({
name: ‘Hanako’,
age: 22
});
// 1. プロパティの値を変更しようとしてみる
immutableUser.age = 30;
console.log(immutableUser.age); // 出力: 22 (変わっていない!)
// 2. 新しいプロパティを追加しようとしてみる
immutableUser.gender = ‘female’;
console.log(immutableUser.gender); // 出力: undefined (追加もできない!)
// 3. プロパティを削除しようとしてみる
delete immutableUser.name;
console.log(immutableUser.name); // 出力: ‘Hanako’ (削除もできない!)
すごいですね!`const`で「住所の変更」を封じ、`Object.freeze()`で「家の中の改築」を封じる。この2つをタッグを組ませることで、初めて「真に堅牢な不変オブジェクト(イミュータブル・データ)」が完成するのです。
—
3. 実務で気をつけるべき「浅い凍結(Shallow Freeze)」の罠
さて、ここでワンランク上のプログラミング知見をお伝えしておきます。実は、`Object.freeze()`には「1階層目しか凍結しない(浅い凍結)」という重要な仕様上の限界があります。
例えば、オブジェクトの中に「ネスト(入れ子)された別のオブジェクト」がある場合を考えてみましょう。
const company = Object.freeze({
name: ‘Tech Corp’,
address: {
city: ‘Tokyo’,
street: ‘Shibuya’
}
});
// 1階層目は凍結されているので変更できない
company.name = ‘Startup Inc’; // 無視される(strictモードではエラー)
// しかし! 2階層目のオブジェクトは…?
company.address.city = ‘Osaka’;
console.log(company.address.city); // 出力: Osaka 😱 変わってしまった!
なぜこうなるのでしょうか?
`Object.freeze(company)` は、`company` が直接持っている `name` や `address` というプロパティの「参照」を凍結しますが、`address` が指し示す「別のオブジェクトの内部」までは自動的に巡回して凍結してくれないからです。
完全な不変性を手に入れるための「ディープ・フリーズ(深層凍結)」
実務で本当に安全なアプリケーション設計(例えばReduxのステート管理や設定ファイルの保持など)を行うときは、ネストしたオブジェクトも含めて再帰的に凍結する「ディープ・フリーズ関数」を自前で用意するのがプロの技です。
以下に、実戦でそのまま使えるユーティリティコードを置いておきますね。
/
- オブジェクトを再帰的に完全凍結する関数
- @param {Object} obj 凍結したいターゲットオブジェクト
- @returns {Object} 凍結されたオブジェクト
/
function deepFreeze(obj) {
// オブジェクトが持つすべてのプロパティ名を取得
const propNames = Object.getOwnPropertyNames(obj);
// プロパティが指し示す先がオブジェクトであれば、再帰的にfreezeする
for (const name of propNames) {
const value = obj[name];
if (value && (typeof value === ‘object’ || typeof value === ‘function’)) {
deepFreeze(value);
}
}
return Object.freeze(obj);
}
// — 使用例 —
const secureConfig = deepFreeze({
appName: ‘SuperApp’,
db: {
host: ‘localhost’,
port: 5432
}
});
// ネストされた値も変更できなくなる!
// secureConfig.db.port = 3306; // 失敗する(strictモードならTypeError)
このように、データの構造が深くなっても「どこを触っても絶対に壊れない(変異しない)」という保証をコードに持たせることで、バグの温床を根絶やしにすることができます。
—
まとめ:堅牢な設計のファーストステップ
今回は、`const`の限界と、それを補う`Object.freeze()`の組み合わせについて解説しました。
- `const` は変数の「再代入」を防ぐが、オブジェクトの「プロパティの書き換え」までは防げない。
- `Object.freeze()` を組み合わせることで、オブジェクトの改変を食い止めることができる。
- ただし、通常の `freeze` は「浅い凍結」なので、ネストしたオブジェクトには再帰的な処理(Deep Freeze)が必要になる。
データを「イミュータブル(不変)」に保つという考え方は、予期せぬバグを防ぎ、コードの予測可能性を劇的に高めてくれるモダン開発の必須教養です。ぜひ、今日のコードから `const + Object.freeze` のコンビネーションを意識して取り入れてみてくださいね。
ここをクリアしたあなたなら、JavaScriptの変数とメモリの挙動はもうバッチリマスターできていますよ!自信を持って次のステップへ進んでいきましょう。