【実務・中級編】【初心者向け】constで定義したオブジェクトは本当に「不変」なのか?:参照と値の挙動を理解する – JavaScriptコア文法・モダン言語仕様と変数・関数解析バイブル

コードレビューをしていて、最も頻繁に遭遇する誤解の一つがこれだ。

> 「`const` で宣言したから、このオブジェクトはイミュータブル(不変)だよね」

残念ながら、答えは明確に「No」だ。

フロントエンドの現場、特にReactやVueなどのモダンなUIライブラリを用いたコンポーネント設計や、Redux/Zustandなどの状態管理、さらには複雑な非同期APIレスポンスを扱うコードベースにおいて、この「`const`=完全な不変」という誤解は、追跡困難なバグ(UIが再描画されない、意図せずデータが書き換わる等)の温床となる。

今日は、V8エンジンがメモリ上で変数をどのように扱い、なぜ `const` がオブジェクトの「中身」を守れないのか、そのメカニズムを低レイヤーの視点から解き明かそう。単なる文法のお勉強ではなく、プロダクション環境で即座に使える「堅牢な設計パターン」まで一気通貫で伝授する。

—

1. V8エンジンのメモリ空間:`const` が守っているのは「値」ではなく「アドレス」だ

JavaScriptの変数を理解する上で最も重要なのは、「プリミティブ型(数値や文字列など)」と「参照型(オブジェクトや配列)」で、V8エンジンがメモリ(ヒープとコールスタック)上に確保する仕組みが根本から異なるという点だ。

プリミティブ型の場合

`const x = 42;` と書いたとき、`x` という箱の中には「42」という実体が直接格納される(実際には最適化されるが概念として)。ここで `x = 100;` と再代入しようとすると、`const` はその「箱の付け替え」を禁止しているため、エラー(`TypeError`)が発生する。

オブジェクト(参照型)の場合

では、`const user = { name: ‘Alice’ };` と書いたときはどうなるか?

1. V8エンジンは、ヒープ領域(動的メモリ領域)のどこかに `{ name: ‘Alice’ }` という実体オブジェクトを生成する。
2. コールスタック上の `user` という変数には、そのオブジェクトが格納されているメモリ上の番地(参照・メモリアドレス)が保存される。
3. `const` が担保しているのは、「`user` という変数に別の番地を上書きさせない(再代入の禁止)」という制約だけだ。

つまり、オブジェクトの「中身(プロパティ)」そのものは `const` の保護範囲外にある。だから、以下のような操作はエラーにならずに実行されてしまう。

const user = { name: ‘Alice’ };

// これは再代入(変数に新しいアドレスを入れる行為)なのでエラーになる
// user = { name: ‘Bob’ }; -> TypeError: Assignment to constant variable.

// しかし、中身の書き換えは「同じアドレスの先にある実体を変更する」だけなので成功する!
user.name = ‘Bob’;
console.log(user.name); // ‘Bob’

この「変数のアドレス(参照)」と「実体(オブジェクト)」の分離を理解していないと、関数間でオブジェクトを渡したときに予期せぬ副作用(サイドエフェクト)を引き起こすことになる。

—

2. 実務で頻出する「意図しない書き換え」のアンチパターン

コードレビューでよく見かける、典型的なバグの構図を見てみよう。非同期APIから取得したユーザー設定をキャッシュし、それを加工して別のコンポーネントに渡すシーンを想定してほしい。

// 【アンチパターン】constで守っているつもりで、実際にはグローバルなキャッシュを汚染する例

const defaultSettings = {
theme: ‘dark’,
notifications: {
email: true,
push: false
}
};

function updateTheme(newSettings, themeMode) {
// constで定義されているから安全だと思い込んでいる
const settings = newSettings;
settings.theme = themeMode;
return settings;
}

// 実行
const updated = updateTheme(defaultSettings, ‘light’);

console.log(updated.theme); // ‘light’
console.log(defaultSettings.theme); // あれ!? ‘light’ に変わってしまっている!

何が起きたか? `const settings = newSettings;` は、オブジェクトの「複製」を作ったのではなく、同じメモリアドレスを指す変数をもう一つ増やしただけ(シャローコピーすらしていない、単なる参照の共有)なのだ。そのため、`settings.theme` の書き換えは、大元の `defaultSettings` までも破壊してしまった。

このようなバグは、大規模なフロントエンドアプリケーションにおいて、どのコンポーネントがいつデータを変えたのか追跡できなくなる「負債」の正体である。

—

3. 堅牢な設計パターン:完全な不変性を担保するアプローチ

では、実務においてオブジェクトや配列の意図しない書き換えを防ぎ、予測可能なクリーンなコードを書くにはどうすればよいか。現代のJavaScript(ES2018以降)におけるベストプラクティスを示す。

パターンA:スプレッド構文(Spread Syntax)によるシャローコピー

最も一般的で、コンポーネントのプロパティ更新などで多用される手法だ。

const defaultSettings = {
theme: ‘dark’,
notifications: { email: true, push: false }
};

function updateThemeSafe(baseSettings, themeMode) {
// スプレッド構文で新しいオブジェクトを生成し、参照を切る
return {
…baseSettings,
theme: themeMode
};
}

const updated = updateThemeSafe(defaultSettings, ‘light’);

console.log(updated.theme); // ‘light’
console.log(defaultSettings.theme); // ‘dark’ (大元は守られている!)

⚠️ 注意点(落とし穴)
スプレッド構文や `Object.assign()` は、あくまで「浅いコピー(Shallow Copy)」だ。階層が深いネスト構造(上記の `notifications` オブジェクトなど)の場合、内側のオブジェクトは依然として参照が共有されている。

const updated2 = updateThemeSafe(defaultSettings, ‘light’);
updated2.notifications.email = false;

console.log(defaultSettings.notifications.email); // false になってしまう!

パターンB:ディープコピージェネレーション(StructuredClone)

ネストしたオブジェクトも含めて完全に独立したコピーを作りたい場合、かつては `JSON.parse(JSON.stringify(obj))` というハックが使われていた(これにはDate型やundefinedが消える致命的な欠点があった)。

現在は、モダンブラウザおよびNode.js(v17以降)の標準APIである `structuredClone()` を使うのが正解だ。

const complexState = {
user: { id: 1, name: ‘Alice’ },
preferences: { theme: ‘dark’, tags: [‘js’, ‘v8’] }
};

// ディープコピー(完全な複製)を作成
const nextState = structuredClone(complexState);

nextState.preferences.tags.push(‘architecture’);

console.log(complexState.preferences.tags); // [‘js’, ‘v8’] (影響を受けない!)
console.log(nextState.preferences.tags); // [‘js’, ‘v8’, ‘architecture’]

パフォーマンス面でのオーバーヘッドは多少あるが、状態管理の堅牢性を担保するためには非常に強力なツールとなる。

パターンC:`Object.freeze()` による「強制的な不変化」

ランタイムレベルでオブジェクトのプロパティ変更を不可能にしたい場合は `Object.freeze()` を使う。

const config = Object.freeze({
API_ENDPOINT: ‘https://api.example.com’,
TIMEOUT: 5000
});

// Strictモード下ではエラー、非Strictモードでも変更は無視される
config.TIMEOUT = 10000;

console.log(config.TIMEOUT); // 5000 のまま

ただし、`Object.freeze()` も「浅い凍結(Shallow Freeze)」であることに注意してほしい。ネストされたオブジェクトの書き換えを防ぐには、再帰的にfreezeを行うユーティリティ関数を書くか、TypeScriptの `readonly` 修飾子や Immutable.js などのライブラリを導入する設計判断が必要になる。

—

4. プロダクションコード例:堅牢なAPIデータハンドラ

最後に、非同期APIから受け取ったデータを破壊せずに安全に加工し、UIへ受け渡す実務的なモジュールの例を示す。TypeScript風の型感をイメージしながら読んでほしい。

/

  • @typedef {Object} UserProfile
  • @property {number} id
  • @property {string} name
  • @property {Object} settings
  • @property {string} settings.theme

/

/

  • APIからのレスポンスデータを安全にイミュータブルに加工するモジュール

/
class UserPreferencesManager {
#internalState;

/

  • @param {UserProfile} initialData

/
constructor(initialData) {
// 外部から渡されたオブジェクトの参照を断ち切るため、ディープコピーして保持
this.#internalState = structuredClone(initialData);
}

/

  • 読み取り専用のスナップショットを返す(外部からの直接書き換えをブロック)
  • @returns {UserProfile}

/
getState() {
return structuredClone(this.#internalState);
}

/

  • テーマ設定を安全に更新する
  • @param {string} newTheme

/
updateTheme(newTheme) {
// イミュータブルな更新パターン
this.#internalState = {
…this.#internalState,
settings: {
…this.#internalState.settings,
theme: newTheme
}
};

// 必要であればここでUIの再描画トリガーやイベントを発火させる
this.#dispatchChangeEvent();
}

#dispatchChangeEvent() {
console.log(‘[System] State has been updated immutably:’, this.getState());
}
}

// — 使用例 —
const apiResponse = {
id: 42,
name: ‘Dev Engineer’,
settings: { theme: ‘dark’ }
};

const manager = new UserPreferencesManager(apiResponse);

// テーマを安全に変更
manager.updateTheme(‘light’);

// 大元の apiResponse は一切汚染されていない
console.log(‘Original API Response theme:’, apiResponse.settings.theme); // ‘dark’

—

チーフアーキテクトからの提言

「`const` を使っているから安全」という慢心は、コードベースがスケールするにつれて、デバッグに何時間も費やす原因となる「技術的負債」の種を蒔いているようなものだ。

  • 変数の宣言における `const` は、あくまで「再代入の抑止(スコープ内でのアドレス固定)」である。
  • オブジェクトや配列を扱う際は常に「このデータはどこから参照されていて、どこで書き換わる可能性があるか」をV8のメモリモデルに投影して思考せよ。
  • データの不変性(Immutability)を担保するためには、スプレッド構文、`structuredClone`、あるいは `Object.freeze` をコンテキストに応じて適切に選択し実装すること。

この原則をチーム全体で共通認識として持てば、あなたの書くコードの堅牢性は劇的に跳ね上がる。さあ、明日からのコードレビューで、メンバーの意識をアップデートしてやってほしい。

タイトルとURLをコピーしました