配列の参照渡しとは何か
配列は「参照型」のオブジェクトです。変数が直接値そのものを持つのではなく、“配列がある場所(参照)”を指しています。ここが重要です:代入や関数引数で配列を渡すと、“配列そのもの”ではなく“参照”がコピーされます。結果として、片方を変更するともう片方にも同じ変更が見える――これが参照渡しの本質です。
代入の挙動(変数間で同じ配列を共有する)
代入は“参照のコピー”
const a = [1, 2, 3];
const b = a; // a と b は同じ配列を指す
b.push(4);
console.log(a); // [1, 2, 3, 4](a も変わる)
console.log(b); // [1, 2, 3, 4]
JavaScriptここが重要です:b = a は“値の複製”ではありません。“指し示し先が同じになる”ため、どちらからの変更も同じ実体に対して行われます。独立させたいなら“コピー(複製)”が必要です。
const と参照の関係(不変なのは“参照先”ではなく“紐付け”)
const a = [1, 2];
a.push(3); // OK(配列の中身は変えられる)
a = [9, 9]; // NG(再代入はできない)
JavaScriptここが重要です:const は“変数に再代入できない”だけです。“参照先の中身”の変更(push, pop, splice)は可能です。“不変”にしたいなら“操作の方針(イミュータブルな書き方)”で担保します。
関数引数の挙動(渡された配列を中から変えると外にも影響)
引数は“参照”で渡される(変更が外へ反映)
function addX(arr) {
arr.push("X");
}
const a = ["A", "B"];
addX(a);
console.log(a); // ["A", "B", "X"](関数内の変更が外へ出る)
JavaScriptここが重要です:関数に渡した配列は、そのまま“同じ配列”です。関数内で破壊的操作(push/splice)をすると、呼び出し側の配列も変わります。安全性や予測可能性のために“返り値で新配列を返す(非破壊)”設計がよく使われます。
非破壊の設計(新配列を返す)
function withX(arr) {
return [...arr, "X"]; // スプレッドでコピー+追加
}
const a = ["A", "B"];
const b = withX(a);
console.log(a); // ["A", "B"](元はそのまま)
console.log(b); // ["A", "B", "X"](新配列)
JavaScriptここが重要です:関数は“入力を変えず、結果を返す”。このパターンにすると、思わぬ副作用が消え、テスト・再利用が容易になります。
コピーの方法(浅いコピーと深いコピー)
浅いコピー(一次元の要素参照だけを複製)
const a = [{ id: 1 }, { id: 2 }];
const b = a.slice(); // または [...a], Array.from(a)
b[0].id = 999;
console.log(a[0].id); // 999(オブジェクト要素は“同じ参照”のまま)
JavaScriptここが重要です:浅いコピーは“配列の並び”だけを複製します。中にオブジェクトや配列が入っていると、それらの参照は共有されます。一次元のプリミティブ(数値・文字列)なら浅いコピーで十分ですが、ネスト構造では注意が必要です。
深いコピー(ネストまで別物にする)
const a = [{ id: 1, tags: ["x", "y"] }];
const b = structuredClone(a); // ブラウザ/Nodeの標準(循環除く多くに対応)
b[0].tags.push("z");
console.log(a[0].tags); // ["x", "y"](元は変わらない)
JavaScriptここが重要です:深いコピーは“中のオブジェクト・配列も別インスタンス”にします。古典的な JSON.parse(JSON.stringify(a)) は関数や undefined を落とすので注意。標準の structuredClone が推奨です。
破壊的操作と非破壊的操作(参照渡し下での安全な選択)
破壊的操作(元配列を直接変える)
const a = [1, 2, 3];
a.push(4); // 破壊的
a.splice(1, 1);// 破壊的
a.sort(); // 破壊的
JavaScriptここが重要です:参照を共有している場合、破壊的操作は“共有している全員”に影響します。共同編集状態ではバグの温床になりがちです。
非破壊的操作(新配列を返すメソッドを選ぶ)
const a = [1, 2, 3];
const b = a.concat(4); // 非破壊
const c = a.slice(0, 2); // 非破壊
const d = a.map(x => x * 2); // 非破壊
const e = [...a, 4]; // 非破壊(スプレッド)
JavaScriptここが重要です:非破壊のメソッドは“元を壊さない”。参照共有でも安全に使え、意図が明確に伝わります。チーム開発や状態管理(React/Vueなど)では非破壊が基本です。
参照共有の影響を最小化する設計(イミュータブル思考)
“入力は不変、出力で変化”にする
function removeById(arr, id) {
const i = arr.findIndex(x => x.id === id);
if (i === -1) return arr; // 見つからなければそのまま返す
return [...arr.slice(0, i), ...arr.slice(i + 1)]; // 新配列
}
JavaScriptここが重要です:受け取った配列を触らず、結果だけ返す。これを統一すると、どこで変化が起きるか追いやすく、バグが激減します。
状態更新の定番(置き換えは map、追加はスプレッド、削除は filter)
const items = [{id:1},{id:2},{id:3}];
// 置き換え
const updated = items.map(x => x.id === 2 ? { ...x, name: "new" } : x);
// 追加
const added = [...items, { id: 4 }];
// 削除
const removed = items.filter(x => x.id !== 2);
JavaScriptここが重要です:非破壊の三種の神器(map / スプレッド / filter)。“参照渡しの副作用”を避けつつ、読みやすい更新が作れます。内側オブジェクトの更新にもスプレッドで“浅いコピー”を取りましょう。
ベストプラクティスと落とし穴
“同じ配列を触っている”ことを忘れない
代入や引数渡しは“参照共有”です。破壊的操作をすると、別名の変数や呼び出し側の状態が同時に変わります。共有を意図しないなら必ずコピーしてから扱います。
浅いコピーの限界に気づく
一次元なら安全でも、ネスト配列・オブジェクトは参照が共有されます。必要に応じて structuredClone(または手書きの再構成)で“深い独立”を確保してください。
const で安心しない
const は再代入を禁止するだけで、中身の変更は止めません。“不変”はコード方針(非破壊の関数とメソッド選択)で担保します。
ライブラリやフレームワークの前提を守る
React や Vue の状態管理は“非破壊更新”が前提です。参照渡しのまま破壊的操作をすると再レンダリングが起きなかったり、差分検知に失敗します。常に新配列を返す更新を心がけましょう。
まとめ
配列は参照型で、代入や引数渡しは“同じ配列を共有する”という意味になります。破壊的操作は共有者全員へ影響するため、基本は“非破壊で新配列を返す”設計に切り替えるのが安全です。コピーは浅いコピー([…] / slice)と深いコピー(structuredClone)の違いを理解し、ネストのあるデータでは深い独立を確保します。const で安心せず、イミュータブル思考(map/filter/スプレッド)を徹底すれば、初心者でも参照渡しの落とし穴を避けて、堅牢で読みやすい配列操作が書けます。
