JavaScript | 関数と参照渡しの注意点

JavaScript JavaScript
スポンサーリンク

いいですね!では オブジェクト を使った実践例を、初心者向けにやさしく丁寧に解説します。関数にオブジェクトを渡したときに「元のオブジェクトが変わる/変わらない」の違い、コピーの作り方、よくある落とし穴までカバーします。


1) まずは挙動の確認(オブジェクトはミュータブル)

オブジェクトを関数に渡すと「参照(住所みたいなもの)」が渡されます。関数内でプロパティを書き換えると元のオブジェクトも変わります。

function changeName(user) {
  user.name = "Taro";
  console.log("関数内:", user);
}

let person = { name: "Hanako", age: 20 };
changeName(person);            // 関数内: { name: "Taro", age: 20 }
console.log("関数外:", person); // 関数外: { name: "Taro", age: 20 }  ← 元のオブジェクトが変わっている
JavaScript

ポイント

  • person の中身が関数内の操作で変わる → オブジェクトはミュータブル(変更可能)。

2) 元を変えたくないとき:コピーを渡す(浅いコピー)

関数の中で元オブジェクトを変更したくない場合は、コピーを作って渡します。まずは簡単な「浅いコピー(shallow copy)」。

function changeNameSafe(user) {
  const copy = { ...user }; // スプレッド構文で浅いコピー
  copy.name = "Taro";
  console.log("関数内(copy):", copy);
}

let person2 = { name: "Hanako", age: 20 };
changeNameSafe(person2);           // 関数内(copy): { name: "Taro", age: 20 }
console.log("関数外:", person2);    // 関数外: { name: "Hanako", age: 20 } ← 元は無事
JavaScript

ポイント

  • { ...user } は一段階だけコピーする(プロパティの参照を新しいオブジェクトにコピー)。
  • 簡単でよく使う方法。

3) 浅いコピーの落とし穴:ネストしたオブジェクト

オブジェクトの中にさらにオブジェクトや配列がある場合、浅いコピーだと内部の参照は共有されます(=意図せず元が変わることがある)。

function addHobby(user) {
  const copy = { ...user };   // 浅いコピー
  copy.hobbies.push("soccer");
  console.log("関数内(copy):", copy);
}

let person3 = { name: "Aki", hobbies: ["reading"] };
addHobby(person3); 
console.log("関数外:", person3); // hobbies: ["reading", "soccer"] ← 元の配列も変わる!
JavaScript

なぜ?
hobbies は配列なので、浅いコピーでは hobbies の「参照」だけがコピーされる。新しいオブジェクトと元のオブジェクトが同じ配列を参照しているため、どちらかで変更すると両方に影響します。


4) 深いコピー(deep copy)で完全に分離する方法

ネストがあるオブジェクトを完全にコピーしたいときは「深いコピー」が必要。代表的な方法を2つ紹介します。

方法A — structuredClone(最新で簡単)

const deep = structuredClone(obj);
JavaScript
  • モダンなブラウザ/環境で使える。関数や循環参照も扱える点が便利。

方法B — JSON.parse(JSON.stringify(obj))

const deep = JSON.parse(JSON.stringify(obj));
JavaScript
  • 古くからある手軽な方法。ただし 関数・undefined・Date・RegExp・循環参照 は正しくコピーできない。

例:深いコピーを使う

function addHobbySafe(user) {
  const deepCopy = structuredClone(user); // または JSON.parse(JSON.stringify(user))
  deepCopy.hobbies.push("soccer");
  console.log("関数内(deepCopy):", deepCopy);
}

let person4 = { name: "Aki", hobbies: ["reading"] };
addHobbySafe(person4);
console.log("関数外:", person4); // 元の person4 は変更されない
JavaScript

5) 実践的なパターン(関数設計のヒント)

  • 副作用を避けたい(元のデータを変更したくない)なら、関数はコピーを作って返す(不変データっぽい扱い)。
function addHobbyImmutable(user, hobby) {
  return { ...user, hobbies: [...user.hobbies, hobby] };
}

const before = { name: "Mika", hobbies: ["swim"] };
const after = addHobbyImmutable(before, "run");
// before は変わらない。after は変更後の新しいオブジェクト。
JavaScript
  • 副作用を使っていい場面(パフォーマンスのためにその場で更新したい)もある → 設計によって選ぶ。

6) よくある質問・落とし穴

Q
「プリミティブ(数値・文字列)はどう渡される?」
A

値(コピー)が渡されるので、関数内で変えても元の変数は変わらない。

Q
「オブジェクトを返さない関数で変更するとどうなる?」
A

返さなくても参照先を書き換えれば外側のオブジェクトは変わる(副作用)。

Q
「JSONで深いコピーすると何がダメ?」
A

Dateは文字列になり、関数・undefined・RegExpは消える/壊れる。循環参照はエラー。


7) 練習問題(手を動かしてみよう)

  1. person = { name: "Ken", scores: [10, 20] }doubleScores 関数で各スコアを2倍にしたい。ただし元の person は変えたくない。どう書く?(ヒント:浅いコピーだけではダメ)
  2. オブジェクトに meta: { created: new Date() } がある。JSON.parse(JSON.stringify(...)) で深いコピーするとどうなる?

解答(簡潔)

function doubleScores(person) {
  const copy = structuredClone(person); // または deep copy
  copy.scores = copy.scores.map(s => s * 2);
  return copy;
}
JavaScript
  1. created は文字列に変換されます(Date オブジェクトが ISO 文字列になる)。元の Date オブジェクトのままではなくなる。

タイトルとURLをコピーしました