JavaScriptを使用して動的なWebアプリケーションを開発する際、複雑な数式や条件分岐を記述することは日常茶飯事です。
しかし、ソースコードが意図した通りに動かない原因の多くは、演算子の優先順位と結合規則に対する誤解にあります。
プログラミング言語には、どの計算を先に行うかというルールがあらかじめ定義されています。
このルールを正しく理解していないと、論理的には正しく見えるコードでも、実行時に予期せぬ計算結果を招き、深刻なバグを引き起こす可能性があります。
本記事では、JavaScriptにおける演算子の優先順位の仕組みを整理し、実務で間違いやすいポイントや、安全なコードを書くための実践的な手法を詳しく解説します。
演算子の優先順位とは何か
演算子の優先順位とは、一つの式の中に複数の演算子が含まれている場合に、どの演算を優先的に評価するかを決定する規則のことです。
数学における「足し算・引き算よりも掛け算・割り算を先に計算する」というルールと基本的には同じです。
例えば、以下のコードを見てみましょう。
let result = 10 + 5 * 2;
console.log(result);
20
この場合、足し算(+)よりも掛け算(\*)の優先順位が高いため、先に 5 * 2 が計算され、その結果である 10 に 10 が加算されます。
もし優先順位が等しいか、足し算の方が高ければ結果は 30 になりますが、JavaScriptの仕様では掛け算が優先されます。
優先順位が引き起こすバグの例
優先順位を意識していないと、特に比較演算子や論理演算子が混在した際に直感とは異なる挙動を示すことがあります。
let isAvailable = true;
let stock = 0;
// 在庫がある、もしくは利用可能フラグが立っていて、かつメンテナンス中でない場合を判定したい
let canBuy = isAvailable || stock > 0 && false;
console.log(canBuy);
true
この例では、論理積(&&)が論理和(||)よりも優先順位が高いため、先に stock > 0 && false が評価されます。
その結果と isAvailable が比較されるため、意図せず true と判定されてしまいます。
このような「一見正しそうに見える論理ミス」を防ぐためには、優先順位の体系的な理解が不可欠です。
結合規則(Associativity)の重要性
優先順位と同じくらい重要な概念が「結合規則」です。
これは、同じ優先順位を持つ演算子が並んでいる場合に、左から右に評価するのか、それとも右から左に評価するのかを決めるルールです。
左結合(Left-to-right)
多くの演算子は左結合です。
例えば、引き算や割り算がこれに該当します。
let value = 10 - 5 - 2;
// (10 - 5) - 2 と同じ
console.log(value);
3
右結合(Right-to-left)
一方で、代入演算子やべき乗演算子は右結合です。
let a, b, c;
a = b = c = 5;
// a = (b = (c = 5)) と同じ
console.log(a, b, c);
let power = 2 ** 3 ** 2;
// 2 ** (3 ** 2) = 2 ** 9 と同じ
console.log(power);
5 5 5
512
特にべき乗演算子(**)の右結合性は、数学的な表記に合わせた仕様となっていますが、他の言語から移行してきたエンジニアにとっては間違いやすいポイントの一つです。
JavaScript演算子の優先順位表
JavaScriptにおける主要な演算子の優先順位を、高い順(先に評価される順)に表にまとめました。
数値が大きいほど優先順位が高くなります。
| 優先順位 | 演算子の種類 | 演算子 | 結合規則 |
|---|---|---|---|
| 18 | グループ化 | ( ) | なし |
| 17 | メンバアクセス / 関数呼び出し | ., [], ?., (), new (引数あり) | 左結合 |
| 16 | 後置増分・減分 / new (引数なし) | ++, --, new | なし / 右結合 |
| 15 | 前置増分・減分 / 論理否定 / 単項演算子 | ++, --, !, +, -, typeof, void, delete, await | 右結合 |
| 14 | べき乗 | \*\* | 右結合 |
| 13 | 乗法 | \*, /, % | 左結合 |
| 12 | 加法 | +, - | 左結合 |
| 11 | ビットシフト | <<, >>, >>> | 左結合 |
| 10 | 関係比較 | <, <=, >, >=, in, instanceof | 左結合 |
| 9 | 等価比較 | ==, !=, ===, !== | 左結合 |
| 8 | ビット論理積 (AND) | & | 左結合 |
| 7 | ビット論理異他的論理和 (XOR) | ^ | 左結合 |
| 6 | ビット論理和 (OR) | | | 左結合 |
| 5 | 論理積 (AND) | && | 左結合 |
| 4 | 論理和 (OR) | || | 左結合 |
| 3 | Null合体演算子 | ?? | 左結合 |
| 2 | 条件 (三項) 演算子 / 代入 / yield | ? :, =, +=, -=, \*\*=, => 等 | 右結合 |
| 1 | カンマ演算子 | , | 左結合 |
この表からわかる通り、グループ化の括弧 ( ) が最も高い優先順位を持っています。
複雑な式を書く際は、この括弧を戦略的に活用することが推奨されます。
実践例で学ぶ優先順位の注意点
ここからは、開発現場で特によく遭遇する「優先順位によるトラブル」の具体例を挙げて解説します。
1. 比較演算子の連鎖
数学のように「10 < x < 20」という条件をJavaScriptでそのまま書くと、意図しない結果になります。
let x = 25;
if (10 < x < 20) {
console.log("範囲内です");
} else {
console.log("範囲外です");
}
範囲内です
なぜ「25」が「10〜20の間」と判定されたのでしょうか。
これは比較演算子が左結合であるため、まず 10 < x が評価され、その結果である true が次に 20 と比較されたからです。
JavaScriptでは、比較時に true は 1、false は 0 として扱われるため、最終的に 1 < 20 となり、常に真となってしまいます。
正しい記述方法は以下の通りです。
if (10 < x && x < 20) {
// 正しい論理比較
}
2. 論理否定(!)とプロパティアクセス
オブジェクトのプロパティが存在するかどうかを確認する際、論理否定演算子の位置を間違えるとエラーの原因になります。
let user = null;
// userが存在しない、もしくはnameプロパティがない場合をチェックしたい
// 誤った例
if (!user.name) {
console.log("名前がありません");
}
このコードを実行すると、TypeError: Cannot read properties of null が発生します。
優先順位表を見ると、メンバアクセス(.)の方が論理否定(!)よりも優先順位が高いため、先に user.name が読み取ろうとされ、user が null であるためにクラッシュするのです。
3. Null合体演算子(??)と論理演算子の混合
モダンなJavaScriptで多用される Null合体演算子(??)は、論理積(&&)や論理和(||)と直接混ぜて使うことができません。
// 構文エラーになる例
let settings = user.theme || "dark" ?? "default";
これは優先順位が曖昧になり、バグを生む可能性が高いために言語仕様で制限されています。
複数の論理演算子を組み合わせる場合は、必ず括弧を使用して優先順位を明示しなければなりません。
// 括弧を使って明示的に記述
let settings = (user.theme || "dark") ?? "default";
三項演算子の入れ子による可読性の低下
条件(三項)演算子(? :)は、簡潔なコードを書くのに便利ですが、優先順位が低く右結合であるため、入れ子(ネスト)にすると非常に読みづらくなります。
let score = 85;
let grade = score > 90 ? "A" : score > 80 ? "B" : score > 70 ? "C" : "D";
console.log(grade);
B
このコードは動作しますが、結合規則を知らない開発者が読むと、どの条件がどこに対応しているのかを瞬時に理解するのは困難です。
また、保守性の観点からも推奨されません。
複雑な条件分岐が必要な場合は、if...else 文を使用するか、早期リターンを用いるべきです。
予期せぬバグを防ぐためのベストプラクティス
演算子の優先順位によるトラブルを未然に防ぎ、チーム全体で読みやすいコードを維持するための指針をいくつか紹介します。
括弧を積極的に活用する
優先順位を暗記しているエンジニアであっても、他人が書いたコードを読み解く際には迷いが生じることがあります。
「優先順位が少しでも曖昧だと感じたら、迷わず括弧で囲む」というルールを徹底しましょう。
括弧を使うことで、計算の意図が明確になり、コンパイラや他の開発者に対して「ここを先に計算させたい」というメッセージを明確に伝えることができます。
// どちらが分かりやすいか?
let val1 = a + b * c / d;
let val2 = a + ((b * c) / d); // 計算順序が明白
複雑な計算式を分割する
一行に多くの演算子を詰め込みすぎると、優先順位の把握が難しくなるだけでなく、デバッグも困難になります。
// 複雑すぎる例
let result = (basePrice * (1 + taxRate) - discount) / conversionRate + shippingFee;
// 分割した例
let priceWithTax = basePrice * (1 + taxRate);
let discountedPrice = priceWithTax - discount;
let convertedPrice = discountedPrice / conversionRate;
let result = convertedPrice + shippingFee;
このように変数を介してステップごとに計算を行うことで、優先順位のミスを物理的に排除でき、各ステップの意味も明確になります。
静的解析ツールの導入
ESLintなどの静的解析ツールを導入し、no-mixed-operators ルールを有効にすることをお勧めします。
このルールは、優先順位が混同されやすい演算子の組み合わせ(例えば + と \*、あるいは && と || など)に対して、括弧の使用を強制するものです。
ツールによって機械的にチェックすることで、ヒューマンエラーを最小限に抑えることが可能になります。
まとめ
JavaScriptの演算子の優先順位と結合規則は、一見すると地味なトピックですが、堅牢なアプリケーションを構築する上での基盤となる重要な知識です。
本記事で解説した以下のポイントを常に意識してください。
- 優先順位の把握:掛け算は足し算より強く、論理積(&&)は論理和(||)より強い。
- 結合規則の理解:代入やべき乗など、右から評価される演算子に注意する。
- 括弧による明示:少しでも複雑な式になる場合は、
( )を使って意図を固定する。 - 無理をしない記述:三項演算子のネストや一行での複雑な計算は避け、可読性を優先する。
プログラムは「動けば良い」というものではありません。
将来の自分やチームメンバーがコードを読んだときに、迷いなく意図を汲み取れる状態にすることこそが、プロフェッショナルなプログラミングと言えます。
演算子の仕様を正しく理解し、予期せぬバグのない高品質なJavaScriptコードを目指しましょう。
