参照という名の「迷宮」:JavaScriptのオブジェクト型と上手く付き合う技術
現場でコードを書いていると、「あれ、値を変えたはずなのに、なぜかあっちの変数まで書き換わってるぞ?」というバグに遭遇することはないだろうか?
特に、Reactのstateを直接いじってしまったり、Reduxのreducerで不意にミューテーション(破壊的変更)を起こして「なぜ再レンダリングされないんだ!」と頭を抱える中級エンジニアは非常に多い。
今日は、JavaScriptにおける「プリミティブ」と「オブジェクト」の決定的な違い、そしてJavaScriptがメモリ上で何を企んでいるのかという「裏側の話」を解き明かしていこう。
—
1. プリミティブとオブジェクトの「立ち位置」の違い
まず前提を揃えよう。JavaScriptのデータ型は、大きくプリミティブ(基本型)とオブジェクト(参照型)に分かれる。
- プリミティブ: `String`, `Number`, `Boolean`, `null`, `undefined`, `Symbol`, `BigInt`
- オブジェクト: `Object`, `Array`, `Function`, `Date`, `RegExp` など
ここでの肝は、変数が何を持っているかだ。プリミティブ型の変数は「値そのもの」をスタック領域に直接保持している。一方で、オブジェクト型の変数が持っているのは「値そのもの」ではなく、そのオブジェクトがメモリ上のどこにあるかを示す「参照(アドレス)」に過ぎない。
この違いが、代入や比較の挙動を根本から変えてしまうんだ。
—
2. 「参照渡し」の罠:なぜ同じものを見てしまうのか
百聞は一見に如かず。まずはこのコードを見てほしい。
// プリミティブの場合:値がコピーされる
let countA = 10;
let countB = countA;
countB = 20;
console.log(countA); // 10 (Aには影響しない)
// オブジェクトの場合:参照先がコピーされる
const userA = { name: ‘Alice’ };
const userB = userA; // userAが指している「メモリ上の場所」をコピーしただけ
userB.name = ‘Bob’; // 「Bob」に変えたのは、userBという箱の中身ではなく、その先にあるメモリ上のオブジェクト
console.log(userA.name); // ‘Bob’ … おい、なぜAliceじゃないんだ!となる瞬間
これが、現場で最もよく見る「バグの温床」だ。`userB = userA` と書いた時点で、二つの変数は同じメモリ領域を指し示す「分身」になってしまう。この特性を知らずに、関数にオブジェクトを渡して、その中でうっかり元のオブジェクトを破壊(ミューテーション)してしまうケースが後を絶たない。
—
3. 実務で役立つ「参照の呪縛」からの脱出術
では、どうすればこの呪縛を解けるのか? 現代のJavaScript開発では、「イミュータブル(不変性)」を意識することが大原則だ。
「元のオブジェクトは絶対に変えない」。これを守るための現実的な手法を紹介する。
A. スプレッド構文によるシャローコピー(浅いコピー)
最も一般的で、現代的な書き方だ。
const original = { id: 1, config: { theme: ‘dark’ } };
// スプレッド構文で新しいオブジェクトを生成する
const copy = { …original };
copy.id = 2;
console.log(original.id); // 1 (影響なし)
// 【注意!】階層が深い場合(ネストされたオブジェクト)
copy.config.theme = ‘light’;
console.log(original.config.theme); // ‘light’ … ああ!浅いコピーでは中身までは守れない!
B. ディープコピーの現実的な解
ネストされたオブジェクトまで安全にコピーしたい場合、古くは `JSON.parse(JSON.stringify(obj))` なんてハックもあったが、日付型や関数が消えるため今は非推奨だ。モダンなブラウザなら `structuredClone` を使おう。
const deepData = {
user: { name: ‘Alice’, settings: { lang: ‘ja’ } }
};
// これが現在の最強解。ブラウザ標準の深いコピー
const safeCopy = structuredClone(deepData);
safeCopy.user.settings.lang = ‘en’;
console.log(deepData.user.settings.lang); // ‘ja’ … 完全に分離できた!
—
4. シニアとしてのアドバイス:型判定の罠
最後に、型判定についても一言。`typeof` は便利だが、オブジェクトに関しては「全部オブジェクト」と返してくるポンコツな一面がある。
console.log(typeof {}); // ‘object’
console.log(typeof []); // ‘object’ (配列もオブジェクトの一種だからだ)
console.log(typeof null); // ‘object’ (これは有名なJavaScriptの設計ミスだ)
実務で「これは配列か?」を判定したいなら、`Array.isArray()` を使うのが鉄則だ。それ以外のオブジェクトの種類を知りたいなら、`Object.prototype.toString.call(value)` を使って詳細を確認する習慣をつけよう。
まとめ
1. オブジェクトは「値」ではなく「参照」を扱う。
2. 代入や関数の引数渡しは、参照のコピーである。
3. ミューテーションを避けるため、スプレッド構文や `structuredClone` で新しいオブジェクトを作る癖をつけろ。
4. `typeof` の限界を知り、正しい判定メソッド(`Array.isArray`など)を選べ。
JavaScriptは自由度が高い分、足元をすくわれやすい言語だ。だが、この「メモリの参照」という概念を理解すれば、もうバグに振り回されることはない。
さあ、エディタを開いて、自分のコードが「参照」を汚染していないかチェックしてみよう。その一歩が、君をただのエンジニアから「アーキテクト」へと引き上げるはずだ。

コメント