頑張らないために頑張る

ゆるく頑張ります

関数の引数に再代入することの可否について考える

Posted at — Jul 23, 2021

再代入ってなにそれ

まぁ次のコードを見てくださいな。

const hoge = arg => {
  let temp = arg + 1;
  return temp;
};

console.log(hoge(1));

関数hogeは入力された値に1を加算して返すだけです。機能としてはそれだけです。ここで注目するのは、加算された結果の格納先です。ここでは変数tempを新しく宣言して格納しています。アロー関数なんだから、return arg + 1;にすればいいじゃん、というのはとりあえずここでは置いておいて。

次に、このコードを少し書き直してみます。

const hoge = arg => {
  arg = arg + 1; // arg += 1;でも同様
  return arg;
};

console.log(hoge(1));

変数を新しく宣言して結果を格納するのではなく、引数として与えられたargに直接再代入しちゃえという上記のコード。つまり、計算結果を関数の引数に再代入しているわけです。もちろん、少なくとも狙ったように入力値が1加算されて出力されます。さきほどのコードと同様に動作するわけです。

つまり、引数に本来格納されている値とは別の値を関数の中で再代入しちゃうようなケースですが、とりあえず文法エラーにならず実行が可能で、少なくともこのケースでは想定通りの挙動をしています。なので、少なくとも言語仕様としては問題ないと言えるわけです。

ただ、言語仕様で問題ないからって多用してもいいようなものなの?コレ。

規約で禁止してたりする

Airbnbのコーディング規約では、関数の引数へ値を再代入することは禁止されています。ESLintにも、再代入を禁止するルールが存在します。

7.13 パラメータを再割り当てしない。eslint: no-param-reassign

const hoge = foo => {
  foo += 1;
  return foo;
}

hoge(1);

つまり上記のような、fooに再代入するのはダメということですね。

ESLintなどでは、「no-param-reassign」という名前でルールが存在します。実際、ESLintが体験できるデモサイトでこのルールを有効にした状態で上記のコードを記述すると、再代入している部分でエラーメッセージが表示されます。ただ、このルールはデフォルト設定だとOFFになっています。そこまで優先度は高くないんだろうか。高くないんだろうな。このあたりのさじ加減は、プロジェクトそのものやメンバーの思想に多少左右されるかもしれません。

なお、ESLintでもオブジェクトに対する再代入は「no-param-reassign」のルールをONにしただけではエラーに対象ではありません。オプションで{ "props": true }とした場合のみ、「プロパティの変更まで監視するよー」とオブジェクトへの変更がエラー扱いになります。

そもそも再代入って危ないんか

まぁそれを言っちゃうとケースバイケースとしか言えないんだけど、ちょっと思考実験をしてみます。

const hoge = (foo) => {
  foo = 'string';
  
  return foo;
}

const fuga = (bar) => {
  bar.prop = 'baz';
  
  return bar;
}

let ham = '';
let eggs = {};

console.log(ham);
console.log(eggs);

hoge(ham);
fuga(eggs);

console.log(ham);
console.log(eggs);

こっちはオブジェクトにプロパティを追加しちゃうケース。

"" 
{} 
"" 
{
  "prop": "baz"
} 

上記のように、オブジェクトの内容が変更されています。対して、文字列が格納されている変数には影響なし。

const hoge = (foo, bar) => {
  foo = 9;
  bar.push(4);
  console.log(`inside function: ${foo}, ${bar}`);
}

let n = 1;
let a = [0, 1, 2, 3];

console.log(`outside function 1: ${n}, ${a}`);
hoge(n, a);
console.log(`outside function 2: ${n}, ${a}`);

こっちは配列に値を追加しちゃうケース。

outside function 1: 1, 0,1,2,3
inside function: 9, 0,1,2,3,4
outside function 2: 1, 0,1,2,3,4

実行してみると、変数naに着目したとき関数hogeを実行する前後でnは変化しないものの、配列であるaは内容が変化してしまっています。関数内で引数に対し操作を行ってしまったため、もともと変数に格納されていた値が揮発してしまっています。

個人的には、これらのような再代入を行うコードが危ないと思っています。

これって再代入が悪いって言うよりさ

自分で書いておいてなんですけど、これは再代入処理が悪いというよりミュータブルとイミュータブルを適切に処理していないために起きている症状なので、再代入の是非を問うケースと考えると少々例えが悪い気もしなくもないな・・・。

前述のようなコードを書くことでオブジェクトなどの内容が書き換わってしまい、場合によってはこのことがバグの原因になる可能性が否めません。なので、オブジェクトに意図せずプロパティを追加しちゃったりするような、本来の想定とは異なる記述をするケースを発見するためにもオプションは有用だと考えます。ただ、単純に引数に再代入するのはダメってのは、チェックする必要があるか?という気がしなくもないんだよなー。

プロパティの誤った追加など、意図しない操作をしてしまう可能性があるのは「オプション」として付属品扱いされている方なので、こちらの方がリスキーだと思うのですがね。

まとめ

なにはともあれ、オブジェクトに対する意図しない操作によりオブジェクトの内容を書き換えてしまい、バグの発生原因を作ることが危惧されるくらいのコードを書く可能性があるなら、もういっそルールで禁止してしまえということも1つの解決策かもしれません。少なくとも自分はESLintの設定をONにしておこうと思います。もちろん、オプション設定でオブジェクトへの誤った操作を防ぐために、です。propsのチェックを優先してもらいたいのは山々ですが、現状の仕様では仕方ありません。

reference

  1. no-param-reassign
  2. 7.13 Never reassign parameters. eslint: no-param-reassign
  3. eslint-no-param-reassign
  4. JavaScriptの仮引数に再代入することの是非
  5. 「Airbnb JavaScript Style Guide」の引数への再代入禁止ルール
  6. Why can a function modify some arguments as perceived by the caller, but not others?
  7. JavaScriptでイミュータブルなプログラミングをする
comments powered by Disqus