JavaScriptにおける参照と値のコピー:なぜ “aaa” が残るのか
JavaScriptの学習過程において、多くのエンジニアが一度は直面する不可解な挙動があります。関数に渡したはずの変数が、関数内での操作によって変更されたり、あるいは逆に、期待した通りに変更されなかったりする現象です。特に「オブジェクト」や「配列」を扱う際、意図せず元のデータが書き換わってしまう、あるいは「なぜか前の値(例:’aaa’)が残っている」という状況は、バグの温床となります。
本稿では、JavaScriptのメモリ管理とデータ型(プリミティブ型と参照型)の性質を深く掘り下げ、なぜこのような現象が起きるのか、そしてそれをどう制御すべきかについて、プロフェッショナルな視点から詳細に解説します。
プリミティブ型と参照型の本質的な違い
JavaScriptのデータ型は、大きく「プリミティブ型」と「参照型(オブジェクト型)」の二つに分類されます。この二つの違いを理解することが、データが「残る」理由を解明する鍵です。
プリミティブ型(String, Number, Boolean, null, undefined, Symbol, BigInt)は、値そのものがメモリのスタック領域に直接保持されます。一方、参照型(Object, Array, Function)は、データ本体がヒープ領域に格納され、変数はそのデータがどこにあるかという「メモリ上のアドレス(参照)」を保持します。
「なぜ ‘aaa’ が残るのか」という疑問は、主に「変数の再代入」と「参照の書き換え」を混同している際に発生します。プリミティブ型の変数は、一度値が代入されると、その変数はその値を占有します。別の変数に代入しても、値はコピーされます。しかし、参照型の場合は、変数同士が「同じメモリ上の場所(参照先)」を指しているため、片方を操作するともう片方にも影響が及びます。
なぜ “aaa” が残るのか:スコープと再代入のメカニズム
ここで、多くの初学者が陥る典型的な例を見てみましょう。
let str = "aaa";
function updateString(s) {
s = "bbb";
}
updateString(str);
console.log(str); // 結果: "aaa"
このコードを実行すると、コンソールには “aaa” が出力されます。なぜ “bbb” にならないのでしょうか。
理由は明確です。`str` はプリミティブ型であり、関数 `updateString` に渡されたのは「値のコピー」です。関数内で `s = “bbb”` と記述したとき、それは関数の引数であるローカル変数 `s` の参照先を、文字列 “aaa” から “bbb” に切り替えただけに過ぎません。元の変数 `str` は依然として “aaa” という値を保持しており、関数内の操作は外部の `str` には何の影響も与えません。
これが「なぜ ‘aaa’ が残るのか」の技術的な回答です。プリミティブ型は不変(Immutable)であり、変数の再代入はあくまで「その変数が指し示す先を別の値に変える」行為であって、値そのものを書き換えているわけではないからです。
参照型の挙動:意図しない書き換えの罠
次に、参照型の場合を見てみましょう。
const obj = { value: "aaa" };
function updateObject(o) {
o.value = "bbb";
}
updateObject(obj);
console.log(obj.value); // 結果: "bbb"
この場合、`obj` は “bbb” に変わります。これは、引数 `o` が `obj` と同じメモリ領域(参照)を指しているため、`o.value` を書き換えることは、元の `obj.value` を書き換えることと同義だからです。
ここで注意が必要なのは、「関数の中で新しいオブジェクトを代入した場合」です。
let obj = { value: "aaa" };
function replaceObject(o) {
o = { value: "ccc" };
}
replaceObject(obj);
console.log(obj.value); // 結果: "aaa"
ここでも “aaa” が残ります。関数内の `o = { … }` は、ローカル変数 `o` の参照先を新しいオブジェクトに書き換えただけであり、元の `obj` が指し示すメモリ領域には何の影響も与えていないからです。
実務におけるベストプラクティス
実務のフロントエンド開発、特にReactやVue.jsといったモダンなフレームワークにおいては、この「参照」の扱いが極めて重要になります。状態管理において、意図せず元のデータが残ったり、逆に意図せず更新されたりするバグを防ぐために、以下の原則を守るべきです。
1. 不変性(Immutability)の保持:
データは直接書き換えるのではなく、コピーを作成して更新するアプローチをとります。JavaScriptの展開演算子(Spread Syntax)を活用しましょう。
// 不変性を保った更新の例
const state = { count: 1, name: "aaa" };
const newState = { ...state, name: "bbb" };
2. 純粋関数(Pure Function)の意識:
関数は引数として渡されたデータを直接変更せず、新しい値を返すように設計します。これにより、副作用(Side Effect)を最小限に抑え、コードの予測可能性を高めます。
3. 深い階層のクローン:
オブジェクトの中にオブジェクトが存在する場合、浅いコピー(Shallow Copy)では不十分なことがあります。必要に応じて `structuredClone` やライブラリ(Lodashの `cloneDeep` など)を使用して、深いコピーを行うことが推奨されます。
// structuredCloneを使用した安全な複製
const original = { a: { b: "aaa" } };
const copied = structuredClone(original);
copied.a.b = "bbb";
console.log(original.a.b); // "aaa" が保持される
まとめ:挙動を理解し、制御する
「なぜ ‘aaa’ が残るのか」という問いに対する答えは、JavaScriptのメモリモデルにおける「値のコピー」と「参照の受け渡し」の明確な区別にあります。
プリミティブ型であれば「値がコピーされて渡されるため、元の変数は影響を受けない」。参照型であれば「参照が渡されるため、プロパティを直接操作すれば影響が及ぶが、変数そのものに新しい値を再代入しても元には影響しない」。
この二つの原則を理解するだけで、フロントエンド開発における多くの不可解なバグを論理的に解決できるようになります。特に非同期処理や大規模な状態管理を行う際には、この知識がエンジニアとしての信頼性を左右します。
コードを書くとき、常に「今、自分は値を操作しているのか、それとも参照を操作しているのか」を自問自答してください。その意識こそが、バグを未然に防ぎ、堅牢なアプリケーションを構築するための第一歩です。JavaScriptの挙動を「魔法」として扱うのではなく、メモリ上のデータ構造として理解したとき、あなたのコードはより確実で、プロフェッショナルなものへと進化するでしょう。

コメント