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

JavaScript JavaScript
スポンサーリンク

JavaScriptの「値渡し」と「参照渡し」を、初心者向けにかみ砕いて

最初はピンと来なくても大丈夫。ここを押さえるだけで、関数の中で「元のデータが勝手に変わってた…!」という事故が減ります。


基本の考え方

  • 基本型は安全(値がコピーされる): 数値、文字列、真偽値などは関数に渡すと「値のコピー」が渡されます。関数の中で変えても、元の変数は変わりません。
  • 参照型は注意(同じ箱を触っている): 配列やオブジェクトは「データの場所(参照)」が渡されます。関数の中で中身をいじると、元のデータも変わります。

まずは基本型の例(安全)

function addOne(n) {
  n = n + 1;          // 関数内のnだけが変わる
  console.log(n);     // 11
}

let x = 10;
addOne(x);
console.log(x);       // 10(元のxは変わらない)
JavaScript
  • ポイント: xの「値」がコピーされて渡されるだけ。元のxには影響なし。

参照型の例(注意)

function pushZero(arr) {
  arr.push(0);        // 同じ配列の中身を直接いじっている
}

let numbers = [1, 2, 3];
pushZero(numbers);
console.log(numbers); // [1, 2, 3, 0](元の配列が変わる)
JavaScript
  • ポイント: 関数に渡したのは「配列の場所」。中身を触ると外の配列も変わる。

「代入」と「中身の変更」の違い

  • 中身の変更は影響する: 配列やオブジェクトに対し、要素やプロパティを変えると外にも反映される。
  • 参照の付け替えは影響しない: 関数の中で「まるごと別の配列」を代入しても、外の変数は古い配列のまま。
function replaceArray(arr) {
  arr = [9, 9, 9];    // arrという「参照」を別の配列に付け替えただけ
}

let nums = [1, 2, 3];
replaceArray(nums);
console.log(nums);     // [1, 2, 3](外は変わらない)
JavaScript
  • ポイント: 外の変数numsが持っている参照は変わってないから、元の配列のまま。

よく起きる落とし穴と回避法

1. 関数が勝手にデータを変える問題(副作用)

  • 落とし穴: 関数が配列やオブジェクトを直接書き換えると、予期せぬ場所でバグに。
  • 回避法: 「コピー」を作ってから操作する。元のデータは触らない。
function addEndImmutable(arr) {
  const copy = [...arr]; // スプレッド構文で浅いコピー
  copy.push('END');
  return copy;           // 新しい配列を返す
}

const original = ['A', 'B'];
const updated = addEndImmutable(original);
console.log(original); // ['A', 'B'](安全)
console.log(updated);  // ['A', 'B', 'END']
JavaScript

2. オブジェクトのプロパティの変更

function levelUp(player) {
  player.level += 1;  // 中身の変更 → 外にも影響
}

const hero = { name: 'Ryu', level: 1 };
levelUp(hero);
console.log(hero.level); // 2(変わる)
JavaScript
  • 回避法: 新しいオブジェクトを作って返す。
function levelUpSafe(player) {
  return { ...player, level: player.level + 1 };
}

const hero2 = { name: 'Ken', level: 1 };
const leveled = levelUpSafe(hero2);
console.log(hero2.level); // 1(安全)
console.log(leveled.level); // 2
JavaScript

実践的な例題で理解を固める

例題1: 関数の中で配列を2倍にする

  • 目的: 元の配列を変えずに、2倍した新しい配列を作る。
function doubleAll(arr) {
  // NG: arr[i] = arr[i] * 2; ← 元の配列が変わる
  const result = [];
  for (let i = 0; i < arr.length; i++) {
    result.push(arr[i] * 2);
  }
  return result;
}

const a = [1, 2, 3];
const doubled = doubleAll(a);
console.log(a);       // [1, 2, 3](安全)
console.log(doubled); // [2, 4, 6]
JavaScript
  • ポイント: 新しい配列resultを使うことで副作用を避ける。

例題2: ユーザー名の配列に「guest」を追加する

  • 目的: 元の配列を変えずに、末尾に”guest”を追加。
function addGuest(names) {
  // スプレッド構文で新配列を作り、末尾に追加
  return [...names, 'guest'];
}

const users = ['alice', 'bob'];
const withGuest = addGuest(users);
console.log(users);      // ['alice', 'bob']
console.log(withGuest);  // ['alice', 'bob', 'guest']
JavaScript

例題3: 設定オブジェクトにデフォルト値を入れる

  • 目的: 足りないキーだけ埋めたい。元の設定は残す。
function applyDefaults(config) {
  const defaults = { theme: 'light', lang: 'ja', debug: false };
  // 右側が優先されるので、configの指定があればそれを使う
  return { ...defaults, ...config };
}

const userConfig = { debug: true };
const finalConfig = applyDefaults(userConfig);
console.log(userConfig);  // { debug: true }
console.log(finalConfig); // { theme: 'light', lang: 'ja', debug: true }
JavaScript

さらに一歩:安全に書くためのコツ

  • constを活用: 参照の付け替えを防げる(ただし中身の変更は防げないので注意)。
  • 「純粋な関数」を意識: 入力を変えず、出力だけ返す関数はテストしやすく、バグが少ない。
  • 浅いコピーと深いコピー: スプレッド構文は浅いコピー。ネストが深いオブジェクトを安全に複製したいなら、必要に応じてstructuredClone()などを検討。
const original = { user: { name: 'A' }, score: 10 };
const shallow = { ...original };           // userの中身は共有される
shallow.user.name = 'B';
console.log(original.user.name); // 'B'(影響する)

const deep = structuredClone(original);    // 深いコピー
deep.user.name = 'C';
console.log(original.user.name); // 'B'(影響しない)
JavaScript

まとめの指針

  • 基本型はコピーで安全。参照型は同じ中身を触るので注意。
  • 元のデータを守りたい時は「コピーを作る→新しい値を返す」。
  • 副作用の少ないコードは、デバッグも理解もラクになる。
タイトルとURLをコピーしました