30歳からのプログラミング

30歳無職から独学でプログラミングを開始した人間の記録。

JavaScriptのショートサーキット評価

ほとんどのプログラミング言語には、ANDORを表す論理演算子が用意されている。
そして、左辺を評価した時点で論理式の結果が確定した場合には右辺の評価を行わないことを、ショートサーキット評価(短絡評価)という。
例えば、A AND Bという論理式があった場合、Afalseなら、その時点で式全体の結果はfalseで確定するため、Bがどうであるかについてはチェックしない。

JavaScript論理和演算子||論理積演算子&&も、ショートサーキット評価を行う。
これを利用することで、コードを簡略化したり、パフォーマンスを向上させたりすることが出来る。

論理演算子の挙動

ショートサーキット評価を利用するためにはまず、論理演算子がどのように動くのかを理解しないといけない。
なお、以下の説明は、どのような結果を生むのかについての説明であり、内部的な挙動については正確ではない可能性がある。

まずは論理和演算子||から。
左のオペランドを真偽値に型変換して評価し、それがtrueだった場合は左オペランドを、falseだった場合は右オペランドを、返す。
オペランドを返す場合、右オペランドは評価自体を行わない。

let value;
console.log(value || 'valueが未定義です。'); // valueが未定義です。
value = 7;
console.log(value || 'valueが未定義です。'); // 7

論理積演算子&&はその逆で、左オペランドfalseだった場合は左オペランドを返し、trueだった場合は右オペランドを返す。
オペランドを返す場合、右オペランドは評価自体を行わない。

let value;
console.log(value && 'valueが未定義です。'); // undefined
value = 7;
console.log(value && 'valueが未定義です。'); // valueが未定義です。

具体的な使い方

ショートサーキット評価を利用することで、条件分岐を簡単に書くことが出来る。

以下は、&&を使った書き方。

function checkValue(arg){
  if(typeof arg === 'number'){ console.log(`${arg}は数値型です。`); }; // 1は数値型です。
  typeof arg === 'number' && console.log(`${arg}は数値型です。`);  // 1は数値型です。
};

checkValue(1);

checkValue()で定義している2つの文は、どちらも同じ意味である。
論理式 && 文は、if(論理式){文};と同じ動きをする。
だがこのような書き方は、あまり見かけない。

よく使われるのは、以下の書き方である。

const useValue = inputtedValue || 10;

useValueを定義しているが、inputtedValueが存在すればその値を、存在しなければ10を、代入している。

複数の論理和演算子

同じ演算子が並んでいる場合、左から順番に評価していく。
そのため、論理和演算子が複数並んでいたときは、左から順番に評価していき、trueに型変換できるものが出た時点で、それを返す。
trueに型変換できるものがなかった場合は、一番右のオペランドを返す。

let hoge, fuga, muu;
console.log(hoge || fuga || muu); // undefined
fuga = 5;
console.log(hoge || fuga || muu); // 5
hoge = 7;
console.log(hoge || foo || muu); // 7

ちなみに、最後の論理式でfooという未定義の変数を使っており、本来ならエラーになる。
だがこの論理式では、hogeを評価した時点で論理式の評価は止まるので、エラーは出ない。このことからも、論理和演算子ではtrueに型変換できるものが出た時点で評価が終わるということを、確認できる。

簡略化を行うべきか

このように、ショートサーキット評価を利用することで、コードを簡略化できる。
だがこれに対しては、批判的な意見も存在する。可読性が落ちるというのが、その理由である。
確かにやり過ぎれば、可読性は落ちるだろう。
だが例えば、先程の複数の論理和演算子などは、むしろ可読性が向上すると思う。

useData = person.address || person.city || person.country;
if(person.address) {
  useData = person.address;
} else if(person.city) {
  useData = person.city;
} else {
  useData = person.country;
};

どちらも同じように動作するが、前者の書き方を知っていると、後者は冗長に感じる。

ショートサーキット評価とパフォーマンス

ショートサーキット評価を上手く使えば、可読性だけでなく、パフォーマンスも向上する。

let value;
function checkFunc(arg){
  console.log('checkFuncを呼び出しました。');
  return arg;
};

// コードA
// 関数を呼び出さない。
value && checkFunc(true) ? console.log('ok') : console.log('ng') ;

// コードB
// 関数を呼び出してしまう。
checkFunc(true) && value ? console.log('ok') : console.log('ng') ;

上記のコードでは、valuecheckFunc(true)が共にtrueとして評価できる場合にokを表示する。そうでない場合はngを表示する。
この例ではvalueundefinedなので、ngが表示される。
それはコードAでもコードBでも変わらない。
&&の左右を入れ替えただけなのだから、最終的な結果はAもBも同じである。

だがパフォーマンスは異なる。
Aの場合、左オペランドfalseなので、その時点で、つまりcheckFunc(true)を呼び出すことなく、論理式は終了する。
Bでは、左オペランドcheckFunc(true)なので、必ずこの関数が呼び出されてしまう。

このように、重い処理を右オペランドに配置することで、無用な呼び出しを避け、必要なときにのみ呼び出すように出来る。

上記の例ではcheckFunc()はログを表示した後に引数を返すだけだが、ここでそれなりに重い処理を行っていた場合、パフォーマンスには差が生じる。

以下の例で、それを確認できる。

let value;
function checkFunc(arg){
  for(let i=0; i < 100; i++){
    1 + 1;
  };
  return arg;
};

const start = new Date();
for(let i=0; i < 1000000; i++){
  value && checkFunc(true) ? 1 : 0 ;  // checkFunc(true)は呼ばれない
};
console.log(new Date() - start);  // 18 〜 21
let value;
function checkFunc(arg){
  for(let i=0; i < 100; i++){
    1 + 1;
  };
  return arg;
};

const start = new Date();
for(let i=0; i < 1000000; i++){
  checkFunc(true) && value ? 1 : 0 ;  // checkFunc(true)を必ず呼び出す
};
console.log(new Date() - start);  // 240 〜 260

実際の秒数はもちろん環境によって異なるが、私の環境では、前者は18〜21ミリ秒、後者は240〜260ミリ秒、といったところだった。
つまり、10倍以上の差が出ていることが分かる。

参考資料