JavaScriptは本当に「値渡し」と「参照渡し」を区別しているのか?—ECMAScript仕様から読み解く
はじめに (対象読者・この記事でわかること)
本記事は、JavaScriptの「オブジェクトは共有渡し(参照渡し)、プリミティブは値渡し」という俗説に違和感を持つ中級者以上のフロントエンド・バックエンド開発者を対象に、ECMAScript仕様の用語と実装挙動を突き合わせて整理します。読み終えると、「引数は常に値渡し」「オブジェクト変数は参照値を値として持つ」という正確な理解に到達し、面接やレビューでの説明、バグの根本原因の切り分けができるようになります。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。 - 変数・スコープ・関数呼び出しの基礎 - プリミティブとオブジェクトの基本的な違い
JavaScriptの代入と引数受け渡しはどうモデル化されているのか
JavaScriptの議論で「オブジェクトは共有渡し、プリミティブは値渡し」と語られることがあります。しかしECMAScript仕様はC/C++の「参照渡し」やC#のrefのような機構を直接的には採用していません。仕様が扱う中心概念は「値(Value)」と「参照(Reference)」ですが、ここでのReferenceは「変数やプロパティを解決するための抽象的な内部レコード」であり、ランタイムのポインタとは異なる内部用語です。
実務上の心構えとしては、次の2点が大枠の真実です。
1) 代入も引数渡しも「値のコピー」です。違いは、その「値」がプリミティブか、オブジェクトを指す参照値(ポインタ様の値)か。
2) 変数にはオブジェクト自体が格納されるのではなく、「ヒープ上のオブジェクトを指す参照値」が格納されます。参照値自体はコピーされますが、コピー先と元の参照値は同じオブジェクトを指すため、オブジェクトの内部を変更すれば双方から観測できます。
このため、「引数は常に値渡し(pass-by-value)」が正確な説明であり、「参照渡し(pass-by-reference)」は誤用です。オブジェクトに関して言えば「参照値を値渡ししている」が実態です。これが「共有渡し」と呼ばれることもありますが、言語仕様の正式用語ではありません。
仕様と挙動を丁寧に読み解く:値、参照値、Referenceレコード、そしてよくある誤解
ここが記事のメインパートです。ECMAScript仕様の用語、コード例、アンチパターン、面接での問答に耐える説明を通して理解を固めます。
ステップ1: まずは最小の実験で直感を整える
次のコードを観察します。
// 1) プリミティブ
function bump(n) {
n++;
}
let a = 1;
bump(a);
console.log(a); // => 1(変わらない)
// 2) オブジェクト
function touch(obj) {
obj.x = 42;
}
const o = { x: 0 };
touch(o);
console.log(o.x); // => 42(変わる)
// 3) オブジェクト参照の再束縛は呼び出し側に影響しない
function reassign(obj) {
obj = { x: 99 }; // パラメータ変数に新しい参照値を代入
}
const p = { x: 0 };
reassign(p);
console.log(p.x); // => 0(変わらない)
観測される事実: - プリミティブは関数内の操作が呼び出し元に影響しない。 - オブジェクトの「内部変更」は呼び出し元へ反映される。 - しかしパラメータ変数そのものを別オブジェクトに再代入しても、呼び出し元の変数は変わらない。
この3つを同時に満たすメンタルモデルは「引数は常に値渡し。オブジェクトの場合、その値は『参照値』であり、参照値のコピーが渡る。よって内部変更は共有されるが、再代入は共有されない」です。
ステップ2: 仕様に沿って用語を正しく整理する
仕様上の用語を実務視点で最低限に噛み砕きます。
1) 値(Value)
ECMAScriptでの値は、Undefined, Null, Boolean, Number, BigInt, String, Symbol, Object など。ここでのObjectはヒープ上に格納されたエンティティへの「参照値」で取り扱われます。変数はこの値を保持します。
2) 変数とEnvironment Record
変数名からその「格納場所」を解決する抽象メカニズムがEnvironment Recordです。get/setが抽象的に定義され、実装(エンジン)が具体化します。
3) Reference(仕様内部の抽象参照)
「識別子を解決した結果」を運ぶ内部レコードで、C++でいう参照渡しではありません。評価時に「どのオブジェクトのどのプロパティか」「どのEnvironment Recordのどのバインディングか」を表すための、コンパイラ・インタプリタ向けの概念です。開発者が触るポインタではない点に注意。
4) 引数渡し
関数呼出しの際、仮引数への束縛は「引数の評価結果である値」をコピーして作られます。プリミティブなら数値や文字列などがコピーされ、オブジェクトなら「参照値」がコピーされます。したがって「pass-by-value」。ただし参照値が同じオブジェクトを指すため、内部のミューテーションは共有されます。
5) 「参照渡し」と誤認されがちなケース
配列にpushする、オブジェクトのプロパティを書き換える、といった操作は「同一のオブジェクトを共有している」からこそ伝播します。これは「参照値がコピーされただけ」でも起きます。反対に、関数内で新しいオブジェクトを代入した場合は、呼び出し元の参照値には影響しません。
コードで強調します。
function mutate(arr) {
arr.push(1); // 共有された配列オブジェクトの内部変更
}
function rebind(arr) {
arr = []; // 形参に新しい参照値を代入(呼び出し元には無関係)
}
const xs = [];
mutate(xs);
console.log(xs); // [1]
rebind(xs);
console.log(xs); // [1] のまま
この挙動は「pass-by-reference(呼び出し側の変数バインディング自体を関数が直接いじれる)」では説明されません。もし本当に参照渡しなら、rebindで呼び出し元の変数が新しい配列を指すはずですが、実際には変わりません。
ハマった点やエラー解決
1) 不変更新が必要なのに意図せず共有ミューテーションしてしまう
リアクティブフレームワークやRedux系のデータ管理では、オブジェクトの共有ミューテーションがバグ源に。スプレッドや構造的共有を使い、意識的に新しいオブジェクトを作る。
// 悪い: 共有オブジェクトを書き換える
state.user.name = "Alice";
// 良い: 浅いコピー or イミュータブル更新
const newState = { ...state, user: { ...state.user, name: "Alice" } };
2) デフォルト引数やオプションオブジェクトの書き換え副作用
関数内で受け取ったオブジェクトをそのまま変更し、呼び出し元で想定外の副作用が起きる。防御的コピーを行う。
function configure(opts = {}) {
const o = { ...opts }; // 防御的コピー
o.timeout ??= 3000;
return o;
}
3) Set/MapやDateなどのミュータブルオブジェクト
浅いコピーでは中身が共有されうる。必要に応じて深いクローンや専用コピーを行う。
const s1 = new Set([1,2]);
const s2 = new Set(s1); // 要素はコピーされるが、各要素がオブジェクトなら共有に注意
4) equals判定の誤解(== vs === vs 同一参照)
参照値が同じであることと、構造的に等しいことは別。オブジェクトの等価は通常「同一参照」かどうかで判定されます。構造比較が必要ならライブラリか自作のディープイコールを用いる。
解決策
- 心構え: 「引数は常に値渡し。オブジェクトは参照値を値としてコピーしている」と口に出せるようにする。
- デザイン: 副作用が問題になる箇所では不変データパターンを採用する。特に状態管理ではミューテーションを避ける。
- API規約: 引数のオブジェクトを関数内で変更しない(もしくは明確にドキュメント化)。戻り値で新しいオブジェクトを返す。
- 型/リンティング支援: TypeScriptでreadonly、ESLintのfunctional-rules、Immerなどを活用。
- テスト: 共有ミューテーションが起きていないかプロパティスナップショットで検証する。
まとめ
本記事では、JavaScriptの引数・代入は「常に値渡し」であり、オブジェクトの場合は「参照値を値としてコピーする」ため内部変更が共有される、という正確なメンタルモデルを整理しました。
- 要点1: 仕様のReferenceは内部概念であり、C系の「参照渡し」とは別物。
- 要点2: オブジェクトは「参照値を値渡し」しているに過ぎず、再代入は共有されない。
- 要点3: 状態管理では不変更新を徹底し、副作用をコントロールする。
これにより、挙動の一貫した説明ができ、バグの芽を早期に摘めます。今後は、配列/Map/Set/Date/TypedArrayなど各ビルトインのミューテーション特性や、構造的共有・イミュータブルデータ構造の実践パターンも解説する予定です。
参考資料
- ECMAScript Language Specification (ECMA-262) https://tc39.es/ecma262/
- MDN: Values, variables, and literals https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures
- MDN: Functions — passing arguments by value https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/arguments
- 2ality: “Primitives vs. objects” https://2ality.com/2011/04/primitives-and-objects.html
- Redux Style Guide: Immutability https://redux.js.org/style-guide/style-guide#treat-reducers-as-pure-functions