JavaScriptの演算子の優先順位と「禁止ルール」の一覧
JavaScriptには他の多くのプログラミング言語と同じく、演算子の優先順位 (operator precedence)があります。ただし、シンプルな前置・後置・中置演算でないケースや、特別な禁止ルールがあるため、一言で説明できない部分もあります。本稿ではECMAScript仕様書の構文 (2022年2月時点) の内容をもとに、JavaScriptにおける演算子の優先順位の詳細の説明を試みます。
まず基本となる優先順位表が以下です。
ただし、種別は以下の通りです。
- prefix (前置演算子) …… もとの式の手前に何個でもつけられる演算子。
- 例:
-~-~x
- 例:
- postfix (後置演算子) …… もとの式の直後に何個でもつけられる演算子。
- 例:
x.foo()`bar`[0]
- 例:
- postfix once …… もとの式の直後に1個だけつけられる演算子。
- 例:
x++は可能だがx++--はパースされない。- 逆に
++--xはパースされるが、構文とは別のルールで禁止される。 (後述)
- 逆に
- 例:
- infixL …… 中置演算子で左結合 (演算子の優先度が同じ場合は左側にあるほうが優先される)
- 例:
0.1 + 1.0 - 1.0は(0.1 + 1.0) - 1.0になる
- 例:
- infixR …… 中置演算子で左結合 (演算子の優先度が同じ場合は右側にあるほうが優先される)
- 例:
2 ** 2 ** 3は2 ** (2 ** 3)になる
- 例:
以降ではいくつか注意すべきケースについて説明します。なお、コーナーケース的な状況についても議論しているため、実用的には出てこないような例が出てくることもありますのでご了承ください。
PrimaryExpression とその亜種
中置・前置・後置記法のどれにも属さない構文はほとんどの場合優先度管理の必要がなく、常に最優先で解釈されます。これには以下の構文が含まれます。
- 括弧
-
ParenthesizedExpression (括弧式) ……
(1 + 2)や(0, m.default)など
-
ParenthesizedExpression (括弧式) ……
- 変数名とその亜種
-
IdentifierReference (任意の変数名) ……
fooなど this-
new……new.targetの形のみ。 -
import……import(),import.metaの形のみ。 -
super……super(),super.,super[]の形のみ。
-
IdentifierReference (任意の変数名) ……
- 単純リテラル
nulltrue,false-
NumericLiteral (数値リテラル) ……
42.0,42nなど -
StringLiteral (文字列リテラル) ……
"foo"など
- 複合リテラル
-
ArrayLiteral (配列リテラル) ……
[1, 2, 3]など -
ObjectLiteral (オブジェクトリテラル) ……
{ foo: 42 }など -
RegularExpressionLiteral (正規表現リテラル) ……
/[a-zA-Z]+/uなど -
TemplateLiteral (テンプレート文字列リテラル) ……
`name: ${name}`など- タグ付きテンプレート文字列リテラルの優先度はこれとは異なる。 (後述)
-
ArrayLiteral (配列リテラル) ……
-
function系の式-
FunctionExpression (function式) ……
function() {}など -
GeneratorExpression (ジェネレーター式) ……
function*() {}など -
AsyncFunctionExpression (async function式) ……
async function() {}など -
AsyncGeneratorExpression (非同期ジェネレーター式) ……
async function*() {}など
-
FunctionExpression (function式) ……
-
ClassExpression (クラス式) ……
class extends Error {}など
LeftHandSideExpression
LeftHandSideExpression は NewExpression, CallExpression, OptionalExpression の非交和です。
複合後置記法
foo.bar は一見すると中置演算子のようにも見えますが、 . の右側には IdentifierReference や PrivateIdentifier などの非常に限定的な内容しか置けません。「式」を基準にすると後置演算子とみなすほうが自然なため、本稿では後置演算子の一種として扱っています。同様に x(...) や x[...], x`...` なども後置演算子の一種として扱っています。
ただし後述するようにOptional chainingとの関係では中置記法と同様の問題が発生します。これは MemberExpression が「式」と「.x や [i] などの後置演算子」を結合する仮想的な中置演算子であると考えるとわかりやすいでしょう。Optional expressionも同等の中置演算子と見なせるため、優先度の問題が発生します。
2種類の new 演算子
new には括弧をともなうものとともなわないものがあります。
new String(); // 括弧をともなうnew演算子
new String; // 括弧をともなわないnew演算子
この2つの new と関数呼び出し構文を組み合わせると、そのままでは曖昧性があることがわかります。
new String(); // 括弧をともなうnew演算子
new String; // 括弧をともなわないnew演算子
String(); // 関数呼び出し
// 優先度を無視すると、 new String(); は次のようにも解釈できる。
(new String)(); // 括弧をともなわないnew演算子 + 関数呼び出し
new (String()); // 括弧をともなわないnew演算子 + 関数呼び出し
これは、 new と () を内側から貪欲に対応させて、余った分は単独で解釈するというルールになっています。
new new f()()();
// ^^ これは単独で使われる
// ^^^--------^^ この2つが対応する
// ^^^--^^ この2つが対応する
new new new f()();
// ^^^ これは単独で使われる
// ^^^--------^^ この2つが対応する
// ^^^--^^ この2つが対応する
ここで、 () は後置記法なので他の後置記法との優先順位の問題は生じません。一方 new は前置記法のため優先順位の問題が生じます。そこで、括弧をともなわない new は . などの後置記法より低い優先度で解決されます。
new foo().bar; // (new foo()).bar
new foo.bar; // new (foo.bar)
括弧をともなう new は括弧の位置にもとづいて優先度が解決されるため、本稿では new() を後置演算子として分類しています。
ここまでの仕様を表現するために、規格中では以下の3つのよく似た非終端記号が登場します。
-
MemberExpression ……
newと()の釣り合いが取れているケース。 -
CallExpression ……
()のほうが多いケース。 -
NewExpression ……
newと()の釣り合いが取れているか、またはnewのほうが多いケース。
Optional chainingとnew
本稿の表では便宜上 ?. を new より低い優先度としていますが、この2つは共存が禁止されているため実際には優先度に上下関係はありません。具体的には以下の禁止ルールがあります。
- 括弧をともなう
new()演算子が?.を挟むのは禁止。-
OptionalChain に
newの規則がないことで実現されている。
-
OptionalChain に
- 括弧をともなわない
new演算子が?.と並ぶのは禁止。- OptionalExpression が式の開始として NewExpression を許可せず、かわりに MemberExpression を使うことで実現されている。
// new foo?.bar(); // Error
// new foo?.bar; // Error
new foo()?.bar; // OK
Optional chainingとMemberExpression/CallExpression
?. は後置記法なので、一見すると同じ後置記法である . や () などと同じ優先度でも問題ないように見えます。しかしOptional chainingは名前の通りチェーンを構成する (そろそろJavaScriptに採用されそうなOptional Chainingを今さら徹底解説 - Qiita / オプショナルチェーンの短絡評価 などを参照) ため、意図的に優先度が下げられています。
// foo に ?.bar.baz が適用されているものとして扱われる
foo?.bar.baz
これを実現するために、 MemberExpression や CallExpression 中の演算子と同じ内容 (new() を除く) が OptionalChain でも繰り返されています。
new, import, super
new, import, super は変数のように使える場合がありますが、これは構文レベルで制限があります。
-
new……new.targetの形のみ。 -
import……import(),import.metaの形のみ。 -
super……super(),super.,super[]の形のみ。
このうち new.target, import.meta, super., super[] については MemberExpression 内に、 import() と super() については CallExpression 内に分岐を追加することで実現されています。そのため、 new と () の対応づけルールは既に説明したものから変わりません。
// import, superのときだけ対応づけが変化する、ということはない。
// new import(); // Error (importは new() できない)
// new super(); // Error (superは new() できない)
UnaryExpression, UpdateExpression
2つに分かれている理由
ExponentiationExpression (**) 内の禁止ルールのために2つに分かれています。 (後述)
前置インクリメントと後置インクリメントの優先順位解決
UnaryExpression/UpdateExpressionには前置演算子と後置演算子が混在しています。しかし後置演算子 (x++ と x--) は左再帰的ではない形で定義されています。
// 後置インクリメント・デクリメントは左再帰的ではないため、次のような書き方は構文レベルで除外されている
// x++--; // Error
これにより同じ非終端記号内でも自動的に後置演算子のほうが優先度が高くなり、曖昧性が解決されます。
// パーサーはこれを ++(x++) としてパースする
// (ただし、実際には構文解析後のEarly Error検査でエラーになってしまう)
++x++;
ExponentiationExpression
** の禁止ルール
ExponentiationExpression において、 ** の右辺に前置演算子 delete, void, typeof, +, -, ~, !, await が出現するのは禁止されています。
// -2 ** 2 // Error
同じ優先度でも ++, -- の出現は許されています。
++x ** 2 // OK
これはあらかじめ UnaryExpression の一部を UpdateExpression として切り出しておき、 ExponentiationExpression の ** 規則の右辺で UnaryExpression のかわりに UpdateExpression を使うことで実現されています。
ShortCircuitExpression
ShortCircuitExpression は LogicalOrExpression と CoalesceExpression の非交和です。
?? の禁止ルール
CoalesceExpression において、 ?? で区切られた部分のいずれかに && または || が出現するのは禁止されています。
// true && true ?? true // Error (括弧が必要)
// true ?? true || true // Error (括弧が必要)
本稿の表では便宜上、 ?? を && / || よりも低い優先度として表記していますが、実際には優先度の上下関係はありません。
これを実現するために、以下のような構文上の工夫が行われています。
- CoalesceExpression は LogicalORExpression を継承せず、これら2つの非交和として ShortCircuitExpression を定義する形にしている。
-
CoalesceExpression において
??の右側を LogicalORExpression ではなく BitwiseORExpression にしている。 -
CoalesceExpression において
??の左側をそのまま再帰的にしてしまうと左端を生成できなくなってしまうので間に CoalesceExpressionHead を置くことで左端を生成できるようにしている。ここで左端を生成するための非終端記号を LogicalORExpression ではなく BitwiseORExpression にしている。
AssignmentExpression
前置記法と中置記法の混在
前置記法と右結合な中置記法はどちらも右再帰的である (→左再帰的ではない) ため、同じ優先度で混在できます。
代入式の禁止ルール
優先度管理の観点からは、代入式 x = y や複合代入式 x += y の左辺には || / ?? までの式を書けてもよいところですが、ほとんどの演算子は左辺には不要のため構文的に禁止されています。
// x + y = 42; // Error
括弧なしで書けるのは LeftHandSideExpression 以上の優先度の演算子のみです。 (それがこの名前の由来だとも言えますが、逆に LeftHandSideExpression だから左辺式だとは言えないところが難しいところです)
どうせ括弧を使えば任意の式をパースさせることができるわけですが、その後の早期エラーの検査で多くが弾かれます。 (後述)
yield
yield には引数ありのバージョンとなしのバージョンがあります。
function* f() {
yield; // 引数なし
yield 42; // 引数あり
}
引数のない yield は一見すると PrimaryExpression に入れてもよさそうですが、もし PrimaryExpression に入れてしまうと以下のような曖昧性が生じてしまいます。
function* f() {
yield+0; // +0をyieldしているのか、無引数のyieldの戻り値に0を足しているのかわからない
}
無引数の yield が AssignmentExpression になっていることで、後続するトークンは ), ], }, ,, ;, : に限定されます。これらはいずれも式を開始しないため曖昧性が解消されます。
代償として、曖昧性が無さそうなケースでも無引数の yield を括弧なしに置けない箇所が多数存在します。
function* f() {
// yield.foo; // Error
// new yield(); // Error
// new yield; // Error
// yield?.foo; // Error
}
ブロック形式のアロー関数
アロー関数の本体には式とブロックの2種類があります。式の場合、アロー関数式は前置演算子と同等とみなせます。
// 構文上は「42 に対して (x) => が前置されている」とみなせる
(x) => 42
ブロックの場合は両端から挟まれているため、原理的には PrimaryExpression としても扱えるはずです。しかし実際には AssignmentExpression に分類されているため、曖昧性が無さそうなケースでも実際にはパースできないことがあります。
// (x) => { return 42; }(); // Error (括弧が必要)
Expression
, の禁止ルール
多くの箇所でコンマ演算子 , が禁止されています (つまり、 Expression のかわりに AssignmentExpression が使われています) 。
- 他の目的で
,を使っている箇所- 配列リテラル内
[1, 2, 3] - オブジェクトリテラル内
({ foo: 42, bar: 100 }) - 実引数リスト内
foo(1, 2, 3) - 配列パターン内
const [x = 1, y = 2] = []; - オブジェクトパターン内
const { foo = 42, bar = 100 } = {}; - 仮引数リスト内
(x = 42, y = 100) => 0
- 配列リテラル内
- 曖昧性はないが
,を禁止している箇所- Computed property name
({ [foo, bar]: 42 }) /* error */ - 条件式のtrue側分岐
true ? 42, 100 : 0 /* error */ -
for-of文内のコレクション式for (const x of 0, []) {} /* error */ -
export default宣言export default 42, 100; /* error */
- Computed property name
自然に発生する禁止ルール
ここまでは人工的な禁止ルールを紹介してきましたが、優先順位表から自然に発生する禁止ルールもあります。これは以下の2つです。
- 前置演算子と、それより優先度の高い前置または中置演算子の組み合わせ。
- 後置演算子と、それより優先度の高い後置または中置演算子の組み合わせ。
上記の組み合わせでは構文木の回転ができないために禁止ルールが生じます。
Optional chainingはやや特殊で、既に紹介したので省略します。それ以外にたとえば以下のような例があります。
// ++/-- と、それより優先度の高い後置演算子の組み合わせ
// x++(); // Error
// delete void typeof await + - ~ ! ++ -- のいずれかと、
// それより優先度の高い前置演算子 (newのみ) の組み合わせ
// new await x; // Error
// yield または => と、
// それより優先度の高い前置演算子
// (delete void typeof await + - ~ ! ++ -- new) の組み合わせ
// async function* f() { await yield x; } // Error
// yield または => と、
// それより優先度の高い中置演算子 (ほぼ全ての中置演算子) の組み合わせ
// null && () => 0; // Error
早期エラー (Early Error)
JavaScriptでは構文解析のあとに静的検査が行われます。これを早期エラー (Early Error) といいます。構文定義上は許されていても、早期エラーで弾かれるために実質的に禁止されている組み合わせがあるため、特に「演算子の優先順位」の議論と関係する部分を紹介します。
なお、早期エラーで弾かれた場合もエラーは SyntaxError という扱いになります。
Optional chainingとタグ付きテンプレートリテラル
Optional chaining内でのタグ付きテンプレートリテラルはパースされますが、早期エラーで禁止されます。このような手順になっているのはASI (自動セミコロン挿入) の挙動を制御するためです。
foo?.`bar`
foo?.bar`baz`
代入可能性
全ての式が代入可能 (= 左辺に置ける) なわけではありません。代入可能かどうかは最終的には実行時に決定されますが、早期エラーでもある程度のチェックが行われます。以下の式で代入可能性がチェックされます。
-
lhs++,lhs--,++lhs,--lhsのlhsの部分 -
lhs = rhsのlhsの部分- ただし、
lhsが配列パターンまたはオブジェクトパターンの場合は、lhs自体ではなく配列・オブジェクトパターンの葉ノードにあたる式に対して代入可能性を検査する。
- ただし、
-
lhs += rhsなどの複合代入演算子 (*=/=%=+=-=<<=>>=>>>=&=^=|=**=&&=||=??=) のlhsの部分
代入可能かどうかはAssignmentTargetTypeというルールで決められています。以下の式だけが代入可能です。
- 識別子 (strict mode中の
eval/argumentsを除く) -
x[...],x.foo(x.#foo,super[...],super.fooも含む) - 代入可能な式を括弧
(...)で括ったもの
x = 42; // OK
x.y = 42; // OK
(f()[0]) += 42; // OK
// (x + y) = 42; // Error
// (0, x) = 42; // Error
// (cond ? low : high) = mid; // Error
なおV8やSpiderMonkeyでは独自拡張として、関数呼び出し f() も構文上は代入可能としているようです。ES5.1までホスト定義関数が参照を返すことが許されていたので、これらの挙動はその名残りかもしれません。
ともあれ、この早期エラーによって以下のような式は実際には禁止されます。
// ++--x; // Error (早期エラー)
// ++x++; // Error (早期エラー)
// ++delete foo.bar; // Error (早期エラー)
これを踏まえると、 ++ / -- は実質的には UnaryExpression の他の演算子よりも優先度が高く、再帰的ではない (++ / -- は複数回連続して適用できない) とも考えられます。
なお delete も引数として参照を期待しますが、この早期エラーの対象にはなっていません (参照以外を渡した場合は何もせずtrueを返す)。strict modeでは特定の delete の使い方が禁止されますが、演算子の優先順位とは関連が薄いため省略します。
その他の主要な構文的曖昧性
ECMAScriptでは演算子の優先順位については文脈自由文法の範囲内で解決しますが、それ以外の曖昧性の多くを先読み指定によって解決しています。
- dangling else
-
elseは内側優先で対応づけられます。
-
-
in演算子-
for文の特定位置ではinが禁止されています。これは演算子の優先順位の問題ではなく、for(;;)とfor-inを先読みなく区別するためのものだと考えられます。
-
- 文・宣言優先ルール
-
{は文頭では曖昧性がありますが、式文よりもブロック文が優先されます。 -
{は=>の直後では曖昧性がありますが、式よりもブロックが優先されます。 -
function,function*,async function,async function*,classは文頭やexport defaultの直後では曖昧性がありますが、式文よりも宣言が優先されます。 - 文頭や
for(の頭などではlet[で始まる場合の曖昧性がありますが、let宣言が優先されます。
-
Discussion