こんにちは!日々の開発、本当にお疲れ様です。
JavaScriptを学び始めると、「変数を作る時はとりあえず `const` を使おう」と教わりますよね。うんうん、それは現代のモダンなJavaScript開発において非常に素晴らしいプラクティスです。
でも、こんな疑問を持ったことはありませんか?
「あれ? `const` って『定数(変わらないもの)』を宣言するキーワードだよね? なのに、中身のオブジェクトのプロパティは書き換えられちゃうの…?」
そうなんです。ここ、JavaScriptの初学者だけでなく、JavaやC#といった他の厳密な型を持つ言語からやってきた開発者が、最初に盛大にハマる「罠」なんですよね。
今日は、V8エンジンのメモリの動きやランタイムの裏側まで少しだけ覗き見ながら、「なぜ `const` のオブジェクトは書き換え可能なのか」「本当の意味でデータを守るにはどうすればいいのか」を、優しく、そして本質的に紐解いていきましょう。ここをクリアすれば、あなたのJavaScriptの基本はもうバッチリマスターできますよ!
—
1. `const` は「値」を守るのではなく「アドレス」を縛る
まずは、`const` というキーワードがJavaScriptのメモリ上で実際に何をしているのか、イメージ図で捉えてみましょう。
JavaScriptの世界では、変数とデータ本体は別々の場所にあります。変数が住んでいるのは「コールスタック」というメモリエリア、そしてオブジェクトのような複雑なデータが住んでいるのは「ヒープ」という広大なメモリエリアです。
`const` がしていることは、「一度変数に入れたメモリアドレス(ヒープへの案内状)を、他のアドレスに書き換えることを禁止する」これだけなんです。
// constでオブジェクトを宣言します
const user = { name: ‘Alice’ };
// これはエラーになります!
// (userという変数に、新しいアドレスを再代入しようとしたため)
// user = { name: ‘Bob’ };
// でも、これは成功します!
// (userが指し示すアドレスの中にあるプロパティを書き換えているだけだから)
user.name = ‘Bob’;
console.log(user.name); // => ‘Bob’ が出力されます
分かりますか? `const` は「変数(Binding)」の再代入をガードしているだけであって、「オブジェクトの内部(Mutableな状態)」そのものを凍結しているわけではないのです。これが、`const` で定義したオブジェクトが書き換え可能な理由の本質です。
—
2. 本当の「不変(イミュータブル)」を手に入れる `Object.freeze()`
「じゃあ、オブジェクトの中身も絶対に書き換えられたくない時はどうすればいいの?」
そんな時に登場するのが `Object.freeze()` です。これを使うと、JavaScriptエンジンに対して「このオブジェクトのプロパティの追加・削除・変更を一切禁止する!」と指示を出すことができます。
実際のコードで動きを見てみましょう。
// Object.freeze() でオブジェクトを凍結する
const frozenUser = Object.freeze({
name: ‘Alice’,
settings: {
theme: ‘dark’
}
});
// プロパティを書き換えようとしてみる
frozenUser.name = ‘Bob’; // 厳格モード(strict mode)ではエラー、通常モードでは静かに無視されます
console.log(frozenUser.name); // => ‘Alice’ のまま!書き換わっていません!
おぉ、すごい!これですべての問題が解決したように見えますよね。
……しかし、伝説のチーフアーキテクトとして、ここであなたに重要な注意点を伝えておかなければなりません。
`Object.freeze()` の限界:シャロー(浅い)フリーズの罠
実は `Object.freeze()` は、一番外側の層しか凍結してくれません(シャロー・フリーズ)。
先ほどのコードの `settings` オブジェクトを見てください。
// ネスト(入れ子)されたオブジェクトの中身を変更してみます
frozenUser.settings.theme = ‘light’;
// アラッ!外側は凍結されていたのに、内側のオブジェクトは書き換わってしまった!
console.log(frozenUser.settings.theme); // => ‘light’ になってしまう
JavaScriptのオブジェクトは、オブジェクトの中に別のオブジェクトを入れる(ネストする)ことがよくあります。`Object.freeze()` は、一番表面の階層にしか効果がないため、奥深くにあるネストされたオブジェクトの改変までは防げないのです。
—
3. 実務で使える!イミュータブルな状態管理のベストプラクティス
では、実務の現場ではどのようにしてデータの不変性を担保し、バグの起きない堅牢なコードを書けば良いのでしょうか?
私たちが日常のアーキテクチャ設計で使っている、代表的なアプローチを2つご紹介しますね。
アプローチA:再帰的なフリーズ関数を作る(ディープ・フリーズ)
もし標準機能だけでオブジェクトの奥深くまで完全に凍結させたい場合は、次のような「再帰関数(自分自身を呼び出す関数)」をユーティリティとして用意します。
/
- オブジェクトを再帰的に完全凍結する関数
- @param {Object} obj – 凍結したいオブジェクト
- @returns {Object} 凍結されたオブジェクト
/
function deepFreeze(obj) {
// オブジェクトのプロパティ名を取得する
const propNames = Object.getOwnPropertyNames(obj);
// すべてのプロパティに対して再帰的にfreezeをかける
propNames.forEach((name) => {
const value = obj[name];
if (value && typeof value === ‘object’) {
deepFreeze(value);
}
});
return Object.freeze(obj);
}
// 使ってみましょう
const appConfig = deepFreeze({
apiEndpoint: ‘https://api.example.com’,
timeout: 5000,
database: {
host: ‘localhost’,
port: 3306
}
});
// これでネストされた内部プロパティも完全に守られます!
// appConfig.database.host = ‘remote-server’; // 失敗(厳格モードではTypeError)
アプローチB:モダンなライブラリやスプレッド構文を活用する
近年のフロントエンド開発(ReactやReduxなど)では、オブジェクトを「凍結」するのではなく、「書き換える代わりに、新しいオブジェクトを作り直す(イミュータブル・アップデート)」という思想が主流です。
JavaScriptが標準で持つスプレッド構文(`…`)を使うと、安全に新しいオブジェクトを作り出すことができます。
const initialState = {
user: ‘Alice’,
isAuthenticated: true,
permissions: [‘read’]
};
// 状態を「変更」するのではなく、新しい状態を「生成」する
const nextState = {
…initialState, // 既存のプロパティを展開しつつ
permissions: […initialState.permissions, ‘write’] // 配列に新しい権限を追加した「新しい配列」で上書き
};
console.log(initialState.permissions); // [‘read’] (元のデータは汚染されていない!)
console.log(nextState.permissions); // [‘read’, ‘write’]
このように、「データを破壊せず、新しいデータを作ってバトンタッチしていく」というスタイルを身につけると、アプリケーションの挙動が劇的に予測しやすくなり、バグの温床を断つことができます。
—
まとめ
いかがでしたでしょうか? 今回のポイントをギュッと凝縮して振り返ってみましょう。
1. `const` は変数の再代入を防ぐものであり、オブジェクトのプロパティの書き換えまでは防げない。
2. オブジェクトの値を固めたい時は `Object.freeze()` を使う。
3. `Object.freeze()` は表面しか凍結しない(シャロー・フリーズ)ため、ネストした構造には再帰的な処理や、新しいオブジェクトを作り直すモダンなアプローチ(スプレッド構文など)を組み合わせる。
JavaScriptの変数とメモリの挙動が少しクリアに見えてくると、コードを書くのがもっともっと楽しくなりますよ。
基礎をしっかりと固めたあなたなら、どんな複雑なアプリケーションの設計も必ずうまくいきます。日々のコーディング、応援しています!