♨️

引数が参照渡しの関数は注意(?)

2024/07/26に公開5

記事の概要

先日、引数が参照(参照渡し)になっている関数はバグを生み出しやすいため注意が必要という話を聞いたので、備忘のために本記事を作成しました。

サンプルコードと解説

以下に、ポケモンのレベルをMaxまで上げる関数を書き、レベルアップ前後のポケモンの状態を出力する処理を書いています。

注意したいパターン(処理1)

boostUpLevelFunction1beforePokémon1の参照を受け取り、参照先のオブジェクトを直接更新する処理となっているため、'beforePokémon1'もレベルアップしてしまっています。

type pokémon = { name: string; level: number };
// 参照先を直接更新する関数
const boostUpLevelFunction1 = (pokémon: pokémon): pokémon => {
  pokémon.level = 99;
  return pokémon;
};
const beforePokémon1: pokémon = { name: "ヒトカゲ", level: 8 };
const afterPokémon1: pokémon = boostUpLevelFunction1(beforePokémon1);

console.log(beforePokémon1); // { name: 'ヒトカゲ', level: 99 }
console.log(afterPokémon1); // { name: 'ヒトカゲ', level: 99 }

改善後のパターン(処理2)

一方でboostUpLevelFunction2は参照を受け取った後に、参照先のオブジェクトをDeepCopyして変数newPokémonにその参照を保存し、DeepCopyしたオブジェクトを更新する処理となっているため、'beforePokémon1'はレベルアップしていません。

type pokémon = { name: string; level: number };
// 参照先を直接更新しない関数
const boostUpLevelFunction2 = (pokémon: pokémon): pokémon => {
  const newPokémon: pokémon = structuredClone(pokémon);
  newPokémon.level = 99;
  return newPokémon;
};

const beforePokémon2: pokémon = { name: "ゼニガメ", level: 8 };
const afterPokémon2: pokémon = boostUpLevelFunction2(beforePokémon2);

console.log(beforePokémon2); // { name: 'ゼニガメ', level: 8 }
console.log(afterPokémon2); // { name: 'ゼニガメ', level: 99 }

まとめ

複雑な処理や利用箇所とは離れた部分で定義されている関数で上記の注意したいパターンのような関数になっていると、新たな処理を追加する際などに元のオブジェクトに影響を与えていると気づけず、想定外の動き(バグ)を生み出しやすくなってしまうとのことです。
ですので、用途によるかもしれませんが可能な場合は特定の処理を関数化する際は引数自体に影響を与えないような形にするのが良さそうです。
もしおかしな点に気づいた方や、もっと良い例など教えていただけるようでしたらコメント等いただけると嬉しいです。
(例が悪い気がするので後で書き直すかも知れないです)

ここまでお読みいただきありがとうございました。

Discussion

junerjuner

それは参照渡し ではなくて、同じインスタンスに対して破壊的操作を行っているだけでは……?

そうえもんそうえもん

コメントありがとうございます!
勉強不足で大変申し訳ないのですが、インスタンスの参照を渡していると思っていたのですが違うのでしょうか?(それは参照渡しではないということでしょうか?)

junerjuner

そうです。
参照渡し で言うところの 参照は つまり 変数のことです。なので インスタンス渡しているからと言って変数にあるインスタンスが挿げ替えられて実引数に反映されることが無いので違います。
(その為 仮引数への代入が実引数に反映される / 実引数への代入が仮引数へ反映される のが参照渡しとなります。)

そのインスタンスの破壊的的操作ができるか否かではなくて 代入が反映されるか否かです。

そうえもんそうえもん

ご返信ありがとうございます。(返信遅くなり申し訳ありません)
「仮引数」「実引数」という言葉を初めて聞き、勉強になりました。
以下の例が参照渡しでの弊害?であって、記事に書いているコードはただ単に引数として渡したインスタンスを変更しているだけだと理解しました。
もし理解した内容に誤りがあれば、ご指摘いただけると大変うれしいです。
コメントいただきありがとうございました!

const pokemon1 = { name: "ゼニガメ", level: 8 };
const pokemon2 = pokemon1;
const levelUp = (pokemon) => {
    pokemon.level = 100
};

levelUp(pokemon2);

console.log(pokemon1); // { name: 'ゼニガメ', level: 100 }
console.log(pokemon2); // { name: 'ゼニガメ', level: 100 }

参考リンク

「仮引数」と「実引数」の違い

そうえもんそうえもん

ご返信ありがとうございます。(返信遅くなり申し訳ありません)
「仮引数」「実引数」という言葉を初めて聞き、勉強になりました。
以下の例が参照渡しでの弊害?であって、記事に書いているコードはただ単に引数として渡したインスタンスを変更しているだけだと理解しました。
もし理解した内容に誤りがあれば、ご指摘いただけると大変うれしいです。
コメントいただきありがとうございました!

const pokemon1 = { name: "ゼニガメ", level: 8 };
const pokemon2 = pokemon1;
const levelUp = (pokemon) => {
    pokemon.level = 100
};

levelUp(pokemon2);

console.log(pokemon1); // { name: 'ゼニガメ', level: 100 }
console.log(pokemon2); // { name: 'ゼニガメ', level: 100 }

参考リンク

「仮引数」と「実引数」の違い