JavaScript | 変数に値を代入するときの仕組み

JavaScript
スポンサーリンク

JavaScriptの「値渡し」と「参照渡し」をやさしく理解する

はじめて出会うと戸惑うポイントだけど、ここを掴むと「なんでバグったの?」がグッと減ります。ゆっくり、具体例と練習問題で体に落とし込みましょう。


基本の考え方

  • 箱のイメージ: 変数は「箱」。プリミティブ型は「中身そのもの」をコピー、オブジェクト型は「同じ箱の場所(地図)=参照」をコピーします。
  • プリミティブ型: 数値、文字列、真偽値、nullundefinedSymbolBigInt。コピーは独立。
  • オブジェクト型: 配列、オブジェクト、関数、日付、正規表現など。コピーは同じものの「共有」になります。

プリミティブ型は「値そのものをコピー」

  • ポイント: 値を渡すたびに新しい独立した値になるので、片方を変えてももう片方は変わりません。
let a = 10;
let b = a;   // 10 をコピー
b = 20;

console.log(a); // 10 (aは変わらない)
console.log(b); // 20
JavaScript
  • よくある場面:
    • 数値計算: 足し算や引き算で元の値が勝手に変わらない。
    • 文字列操作: 文字列は不変(イミュータブル)なので、代入先とは独立。

オブジェクト型は「同じものを共有(参照渡し)」

  • ポイント: 代入すると「参照(場所)」がコピーされ、同じ実体を一緒に触ることになります。
let a = [10, 20];
let b = a;        // 同じ配列への参照をコピー
b[0] = 99;

console.log(a); // [99, 20] (aも変わる)
console.log(b); // [99, 20]
JavaScript
  • オブジェクトでも同じ:
let user1 = { name: "Miki", age: 20 };
let user2 = user1;         // 同じオブジェクトを参照
user2.age = 21;

console.log(user1.age); // 21(user1も変わる)
JavaScript
  • 落とし穴: 「別の変数に入れたから安全」と思って変更すると、元のデータも変わっていてバグの原因になります。

##「ちゃんとコピーしたい」ときの方法

配列の浅いコピー(一次元・中身がプリミティブ中心ならOK)

  • スプレッド構文:
const a = [1, 2, 3];
const b = [...a];  // 新しい配列を作る
b[0] = 9;

console.log(a); // [1, 2, 3]
console.log(b); // [9, 2, 3]
JavaScript
  • slice:
const a = [1, 2, 3];
const b = a.slice();
JavaScript

オブジェクトの浅いコピー

  • スプレッド構文:
const user1 = { name: "Ryo", score: 80 };
const user2 = { ...user1 };
user2.score = 90;

console.log(user1.score); // 80
console.log(user2.score); // 90
JavaScript
  • Object.assign:
const user2 = Object.assign({}, user1);
JavaScript

浅いコピーの注意(入れ子の中身は共有される)

  • ポイント: ネストした配列・オブジェクトの「中の中」は同じ参照を持ち続けます。
const a = [{ x: 1 }, { x: 2 }];
const b = [...a];      // 浅いコピー

b[0].x = 999;

console.log(a[0].x); // 999(中身のオブジェクトは共有されてる)
JavaScript

深いコピー(入れ子も含めて完全に別物)

  • 手軽な方法(JSON): 単純なデータなら便利。ただし関数・Dateundefinedは消えるため注意。
const a = { p: 1, q: { r: 2 } };
const b = JSON.parse(JSON.stringify(a));

b.q.r = 999;
console.log(a.q.r); // 2
JavaScript
  • より安全な方法: structuredClone(a)(対応環境なら推奨)。関数はコピーされないが、DateMapなど多くの型に対応。
const a = { when: new Date(), list: [{ v: 1 }] };
const b = structuredClone(a);
JavaScript

よくある失敗と回避パターン

  • 同じデータを見ているときに部分更新する
    • 失敗例: リストを別の変数に入れてから編集したら、元の画面のリストも勝手に変わる。
    • 回避: 配列・オブジェクトは必要な箇所だけ新しいコピーを作る。
  • 浅いコピーで安心してしまう
    • 失敗例: スプレッドでコピーしたのに、中の配列やオブジェクトが連動して変わる。
    • 回避: ネストがあるなら深いコピー。あるいは更新箇所だけイミュータブルに作り直す。
  • 関数・日付・undefinedがJSONで消える
    • 回避: structuredCloneを検討。ダメなら手動で各フィールドを新規生成。

練習問題

例題1:プリミティブ型の独立性

  • 問題: 次の出力はどうなる?
let x = "Hello";
let y = x;
y = "World";

console.log(x);
console.log(y);
JavaScript
  • 答えの考え方: 文字列はプリミティブ。コピーは独立。
  • 期待出力:
Hello
World

例題2:参照の共有(配列)

  • 問題: 次の出力はどうなる?
let a = [1, 2];
let b = a;
a.push(3);
b[0] = 9;

console.log(a);
console.log(b);
JavaScript
  • 答えの考え方: abは同じ配列を共有。
  • 期待出力:
[9, 2, 3]
[9, 2, 3]

例題3:浅いコピーとネスト

  • 問題: 出力は?
const original = [{ n: 1 }, { n: 2 }];
const copy = [...original];
copy[1].n = 100;

console.log(original[1].n);
JavaScript
  • 答えの考え方: 浅いコピーなので内側のオブジェクトは共有。
  • 期待出力:
100

例題4:安全にコピーしてから編集

  • 問題: 配列 [ {x:1}, {x:2} ] の完全コピーを作って、コピー側の最初の要素だけ x=999 にしたい。元は変えないで。
  • 解答例(structuredClone推奨):
const arr = [{ x: 1 }, { x: 2 }];
const safe = structuredClone(arr);

safe[0].x = 999;

console.log(arr[0].x);  // 1 (元は変わらない)
console.log(safe[0].x); // 999
JavaScript
  • 解答例(JSONで代用・単純データ限定):
const arr = [{ x: 1 }, { x: 2 }];
const safe = JSON.parse(JSON.stringify(arr));

まとめと次の一歩

  • プリミティブは独立、オブジェクトは共有。
  • 浅いコピーは「形」だけ別、深いコピーは「中身」まで別。
  • ネストがあるデータは深いコピーか、更新箇所だけ作り直す。
タイトルとURLをコピーしました