Operatorpräzedenz
Operatorpräzedenz bestimmt, wie Operatoren in Bezug zueinander geparst werden. Operatoren mit höherer Präzedenz werden zu den Operanden von Operatoren mit niedrigerer Präzedenz.
Probieren Sie es aus
console.log(3 + 4 * 5); // 3 + 20
// Expected output: 23
console.log(4 * 3 ** 2); // 4 * 9
// Expected output: 36
let a;
let b;
console.log((a = b = 5));
// Expected output: 5
Präzedenz und Assoziativität
Betrachten Sie einen Ausdruck, der durch die folgende Darstellung beschreibbar ist, wobei sowohl OP1 als auch OP2 Platzhalter für Operatoren sind.
a OP1 b OP2 c
Die obige Kombination hat zwei mögliche Interpretationen:
(a OP1 b) OP2 c a OP1 (b OP2 c)
Welche Interpretation die Sprache übernimmt, hängt von der Identität von OP1 und OP2 ab.
Wenn OP1 und OP2 unterschiedliche Präzedenzebenen haben (siehe die Tabelle unten), wird der Operator mit der höheren Präzedenz zuerst ausgeführt und die Assoziativität spielt keine Rolle. Beachten Sie, wie Multiplikation eine höhere Präzedenz als Addition hat und zuerst ausgeführt wird, obwohl die Addition im Code zuerst steht.
console.log(3 + 10 * 2); // 23
console.log(3 + (10 * 2)); // 23, because parentheses here are superfluous
console.log((3 + 10) * 2); // 26, because the parentheses change the order
Innerhalb von Operatoren mit derselben Präzedenz gruppiert die Sprache sie nach Assoziativität. Linksassoziativität (von links nach rechts) bedeutet, dass es als (a OP1 b) OP2 c interpretiert wird, während Rechtsassoziativität (von rechts nach links) bedeutet, dass es als a OP1 (b OP2 c) interpretiert wird. Zuweisungsoperatoren sind rechtsassoziativ, daher können Sie schreiben:
a = b = 5; // same as writing a = (b = 5);
mit dem erwarteten Ergebnis, dass a und b den Wert 5 erhalten. Dies liegt daran, dass der Zuweisungsoperator den Wert zurückgibt, der zugewiesen wird. Zuerst wird b auf 5 gesetzt. Dann wird a ebenfalls auf 5 gesetzt — den Rückgabewert von b = 5, also dem rechten Operanden der Zuweisung.
Als weiteres Beispiel hat der einzigartige Exponentialoperator eine Rechtsassoziativität, während andere arithmetische Operatoren eine Linksassoziativität haben.
const a = 4 ** 3 ** 2; // Same as 4 ** (3 ** 2); evaluates to 262144
const b = 4 / 3 / 2; // Same as (4 / 3) / 2; evaluates to 0.6666...
Operatoren werden zunächst nach Präzedenz gruppiert und dann, für angrenzende Operatoren mit der gleichen Präzedenz, nach Assoziativität. Wenn Sie also Division und Exponentiation mischen, kommt die Exponentiation immer vor der Division. Zum Beispiel ergibt 2 ** 3 / 3 ** 2 das Ergebnis 0,8888888888888888, da es dasselbe ist wie (2 ** 3) / (3 ** 2).
Für vorangestellte unäre Operatoren nehmen wir an, wir haben folgendes Muster:
OP1 a OP2 b
wobei OP1 ein vorangestellter unärer Operator und OP2 ein binärer Operator ist. Wenn OP1 eine höhere Präzedenz als OP2 hat, dann würde es als (OP1 a) OP2 b gruppiert werden; andernfalls wäre es OP1 (a OP2 b).
const a = 1;
const b = 2;
typeof a + b; // Equivalent to (typeof a) + b; result is "number2"
Wenn der unäre Operator auf dem zweiten Operanden ist:
a OP2 OP1 b
Dann muss der binäre Operator OP2 eine niedrigere Präzedenz als der unäre Operator OP1 haben, damit es als a OP2 (OP1 b) gruppiert wird. Zum Beispiel ist das folgende ungültig:
function* foo() {
a + yield 1;
}
Da + eine höhere Präzedenz als yield hat, würde dies zu (a + yield) 1 werden — aber da yield ein reserviertes Wort in Generatorfunktionen ist, wäre dies ein Syntaxfehler. Glücklicherweise haben die meisten unären Operatoren eine höhere Präzedenz als binäre Operatoren und leiden nicht unter diesem Problem.
Wenn wir zwei vorangestellte unäre Operatoren haben:
OP1 OP2 a
Dann muss der unäre Operator, der näher am Operanden OP2 steht, eine höhere Präzedenz als OP1 haben, damit es als OP1 (OP2 a) gruppiert wird. Es ist möglich, es anders herum zu bekommen und mit (OP1 OP2) a zu enden:
async function* foo() {
await yield 1;
}
Da await eine höhere Präzedenz als yield hat, würde dies zu (await yield) 1 werden, was darauf wartet, dass ein Bezeichner namens yield geladen wird, und einen Syntaxfehler verursacht. Ähnlich, wenn Sie new !A; haben, da ! eine niedrigere Präzedenz als new hat, würde dies zu (new !) A werden, was offensichtlich ungültig ist. (Dieser Code sieht sowieso unsinnig aus, da !A immer einen booleschen Wert erzeugt, keine Konstrukturfunktion.)
Für nachgestellte unäre Operatoren (nämlich ++ und --) gelten dieselben Regeln. Zum Glück haben beide Operatoren eine höhere Präzedenz als jeder binäre Operator, sodass die Gruppierung immer so ausfällt, wie Sie es erwarten würden. Darüber hinaus ergibt ++ einen Wert, nicht eine Referenz, daher können Sie auch keine mehrfachen Inkremente verketteln.
let a = 1;
a++++; // SyntaxError: Invalid left-hand side in postfix operation.
Operatorpräzedenz wird rekursiv gehandhabt. Betrachten wir zum Beispiel diesen Ausdruck:
1 + 2 ** 3 * 4 / 5 >> 6
Zuerst gruppieren wir Operatoren mit unterschiedlicher Präzedenz nach abnehmenden Präzedenzebenen.
- Der
**-Operator hat die höchste Präzedenz, daher wird er zuerst gruppiert. - Um die
**-Ausdruck herum hat es*auf der rechten Seite und+auf der linken Seite.*hat eine höhere Präzedenz, daher wird es zuerst gruppiert.*und/haben in etwa die gleiche Präzedenz, daher gruppieren wir sie zunächst zusammen. - Um den in 2 gruppierten
*//-Ausdruck herum, weil+eine höhere Präzedenz als>>hat, wird ersteres gruppiert.
(1 + ( (2 ** 3) * 4 / 5) ) >> 6
// │ │ └─ 1. ─┘ │ │
// │ └────── 2. ───────┘ │
// └────────── 3. ──────────┘
Innerhalb der *//-Gruppe, da sie beide linksassoziativ sind, wird der linke Operand gruppiert.
(1 + ( ( (2 ** 3) * 4 ) / 5) ) >> 6
// │ │ │ └─ 1. ─┘ │ │ │
// │ └─│─────── 2. ───│────┘ │
// └──────│───── 3. ─────│──────┘
// └───── 4. ─────┘
Beachten Sie, dass Operatorpräzedenz und Assoziativität nur die Auswertungsreihenfolge von Operatoren (die implizite Gruppierung) beeinflussen, aber nicht die Auswertungsreihenfolge von Operanden. Die Operanden werden immer von links nach rechts ausgewertet. Die Ausdrücke mit höherer Präzedenz werden immer zuerst ausgewertet, und ihre Ergebnisse werden dann gemäß der Reihenfolge der Operatorpräzedenz zusammengesetzt.
function echo(name, num) {
console.log(`Evaluating the ${name} side`);
return num;
}
// Exponentiation operator (**) is right-associative,
// but all call expressions (echo()), which have higher precedence,
// will be evaluated before ** does
console.log(echo("left", 4) ** echo("middle", 3) ** echo("right", 2));
// Evaluating the left side
// Evaluating the middle side
// Evaluating the right side
// 262144
// Exponentiation operator (**) has higher precedence than division (/),
// but evaluation always starts with the left operand
console.log(echo("left", 4) / echo("middle", 3) ** echo("right", 2));
// Evaluating the left side
// Evaluating the middle side
// Evaluating the right side
// 0.4444444444444444
Wenn Sie mit Binärbäumen vertraut sind, denken Sie darüber nach in Bezug auf eine post-order Traversierung.
/
┌────────┴────────┐
echo("left", 4) **
┌────────┴────────┐
echo("middle", 3) echo("right", 2)
Nachdem alle Operatoren richtig gruppiert wurden, würden die binären Operatoren einen Binärbaum bilden. Die Auswertung beginnt mit der äußeren Gruppe — das ist der Operator mit der niedrigsten Präzedenz (/ in diesem Fall). Der linke Operand dieses Operators wird zuerst ausgewertet, der aus Operatoren mit höherer Präzedenz bestehen kann (wie einem Aufrufausdruck echo("left", 4)). Nachdem der linke Operand ausgewertet wurde, wird der rechte Operand auf die gleiche Weise ausgewertet. Daher würden alle Blattknoten — die echo()-Aufrufe — von links nach rechts besucht, unabhängig von der Präzedenz der sie verbindenden Operatoren.
Kurzschlussauswertung
Im vorherigen Abschnitt haben wir gesagt, "die Ausdrücke mit höherer Präzedenz werden immer zuerst ausgewertet" — das ist allgemein wahr, aber es muss ergänzt werden mit der Anerkennung der Kurzschlussauswertung, bei der ein Operand möglicherweise gar nicht ausgewertet wird.
Kurzschlussauswertung ist ein Fachbegriff für konditionale Auswertung. Zum Beispiel wird im Ausdruck a && (b + c), wenn a falsy ist, der Unterausdruck (b + c) nicht einmal ausgewertet, selbst wenn er gruppiert ist und somit eine höhere Präzedenz als && hat. Man könnte sagen, dass der logische UND-Operator (&&) "kurzgeschlossen" ist. Zusammen mit logischem UND gehören zu den kurzgeschlossenen Operatoren auch logisches ODER (||), der nullische Kohäsionsoperator (??) und das optionale Chaining (?.).
a || (b * c); // evaluate `a` first, then produce `a` if `a` is "truthy"
a && (b < c); // evaluate `a` first, then produce `a` if `a` is "falsy"
a ?? (b || c); // evaluate `a` first, then produce `a` if `a` is not `null` and not `undefined`
a?.b.c; // evaluate `a` first, then produce `undefined` if `a` is `null` or `undefined`
Bei der Auswertung eines kurzgeschlossenen Operators wird immer der linke Operand ausgewertet. Der rechte Operand wird nur ausgewertet, wenn der linke Operand das Ergebnis der Operation nicht bestimmen kann.
Hinweis:
Das Verhalten der Kurzschlussauswertung ist in diesen Operatoren fest verankert. Andere Operatoren würden immer beide Operanden auswerten, unabhängig davon, ob das tatsächlich nützlich ist — zum Beispiel wird NaN * foo() immer foo aufrufen, selbst wenn das Ergebnis niemals etwas anderes als NaN wäre.
Das vorherige Modell der post-order Traversierung bleibt bestehen. Nachdem jedoch der linke Unterbaum eines kurzgeschlossenen Operators besucht wurde, entscheidet die Sprache, ob der rechte Operand ausgewertet werden muss. Ist dies nicht der Fall (zum Beispiel, weil der linke Operand von || bereits truthy ist), wird das Ergebnis direkt zurückgegeben, ohne den rechten Unterbaum zu besuchen.
Betrachten Sie diesen Fall:
function A() { console.log('called A'); return false; }
function B() { console.log('called B'); return false; }
function C() { console.log('called C'); return true; }
console.log(C() || B() && A());
// Logs:
// called C
// true
Nur C() wird ausgewertet, obwohl && eine höhere Präzedenz hat. Das bedeutet nicht, dass || in diesem Fall eine höhere Präzedenz hat — gerade weil (B() && A()) eine höhere Präzedenz hat, wird es als Ganzes vernachlässigt. Wenn es so umgestellt wäre:
console.log(A() && B() || C());
// Logs:
// called A
// called C
// true
dann würde der Kurzschlusseffekt von && nur verhindern, dass B() ausgewertet wird, aber weil A() && B() als Ganzes false ist, würde C() immer noch ausgewertet.
Beachten Sie jedoch, dass Kurzschluss nicht das endgültige Auswertungsergebnis ändert. Es beeinflusst nur die Bewertung von Operanden, nicht wie Operatoren gruppiert werden — wenn die Bewertung von Operanden keine Nebenwirkungen hat (zum Beispiel, in die Konsole zu protokollieren, Variablen zuzuweisen, einen Fehler zu werfen), wäre Kurzschluss überhaupt nicht beobachtbar.
Die Zuweisungsgegenstücke dieser Operatoren (&&=, ||=, ??=) sind ebenfalls kurzgeschlossen. Sie sind auf eine Weise kurzgeschlossen, dass die Zuweisung überhaupt nicht stattfindet.
Tabelle
Die folgende Tabelle listet die Operatoren in der Reihenfolge von der höchsten Präzedenz (18) zur niedrigsten Präzedenz (1) auf.
Mehrere allgemeine Anmerkungen zur Tabelle:
- Nicht alle hier enthaltenen Syntaxen sind "Operatoren im strengen Sinne". Zum Beispiel werden Spread
...und Pfeil=>typischerweise nicht als Operatoren angesehen. Dennoch haben wir sie aufgenommen, um zu zeigen, wie eng sie im Vergleich zu anderen Operatoren/Ausdrücken gebunden sind. - Einige Operatoren haben bestimmte Operanden, die Ausdrücke erfordern, die schmaler sind als die von Operatoren mit höherer Präzedenz erzeugten. Zum Beispiel muss die rechte Seite des Zugriffsoperators
.(Präzedenz 17) ein Bezeichner anstelle eines gruppierten Ausdrucks sein. Die linke Seite von Pfeil=>(Präzedenz 2) muss eine Argumentliste oder ein einzelner Bezeichner statt eines zufälligen Ausdrucks sein. - Einige Operatoren haben bestimmte Operanden, die Ausdrücke akzeptieren, die breiter sind als die von Operatoren mit höherer Präzedenz erzeugten. Zum Beispiel kann der klammerumfasste Ausdruck der Klammernotation
[ … ](Präzedenz 17) ein beliebiger Ausdruck sein, sogar kommaverbundene (Präzedenz 1). Diese Operatoren handeln, als ob dieser Operand "automatisch gruppiert" wäre. In diesem Fall werden wir die Assoziativität weglassen.
| Präzedenz | Assoziativität | Individuelle Operatoren | Anmerkungen |
|---|---|---|---|
| 18: Gruppierung | n/a | Grouping(x) |
[1] |
| 17: Zugriff und Aufruf | von links nach rechts | Zugriff auf Memberx.y |
[2] |
Optionales Chainingx?.y |
|||
| n/a |
Berechneter Memberzugriffx[y]
|
[3] | |
new mit Argumentlistenew x(y) |
[4] | ||
Funktionsaufrufx(y)
|
|||
import(x) |
|||
| 16: new | n/a | new ohne Argumentlistenew x |
|
| 15: nachgestellte Operatoren | n/a |
Nachgestellter Inkrementx++
|
[5] |
Nachgestellter Dekrementx--
|
|||
| 14: vorangestellte Operatoren | n/a |
Vorangestellter Inkrement++x
|
[6] |
Vorangestellter Dekrement--x
|
|||
Logisches NICHT!x
|
|||
Bitweise NICHT~x
|
|||
Unäres Plus+x
|
|||
Unäres Minus-x
|
|||
typeof x |
|||
void x |
|||
delete x |
[7] | ||
await x |
|||
| 13: Exponentiation | von rechts nach links |
Exponentiationx ** y
|
[8] |
| 12: multiplicative Operatoren | von links nach rechts |
Multiplikationx * y
|
|
Divisionx / y
|
|||
Restx % y
|
|||
| 11: additive Operatoren | von links nach rechts |
Additionx + y
|
|
Subtraktionx - y
|
|||
| 10: Bitverschiebung | von links nach rechts |
Linksverschiebungx << y
|
|
Rechtsverschiebungx >> y
|
|||
Rechtsverschiebung ohne Vorzeichenx >>> y
|
|||
| 9: relationale Operatoren | von links nach rechts |
Weniger alsx < y
|
|
Weniger als oder gleichx <= y
|
|||
Größer alsx > y
|
|||
Größer als oder gleichx >= y
|
|||
x in y |
|||
x instanceof y |
|||
| 8: Gleichheitsoperatoren | von links nach rechts |
Gleichheitx == y
|
|
Ungleichheitx != y
|
|||
Strikte Gleichheitx === y
|
|||
Strikte Ungleichheitx !== y
|
|||
| 7: Bitweises UND | von links nach rechts |
Bitweises UNDx & y
|
|
| 6: Bitweises XOR | von links nach rechts |
Bitweises XORx ^ y
|
|
| 5: Bitweises ODER | von links nach rechts |
Bitweises ODERx | y
|
|
| 4: Logisches UND | von links nach rechts |
Logisches UNDx && y
|
|
| 3: Logisches ODER, Null-Kohäsion | von links nach rechts |
Logisches ODERx || y
|
|
Null-Kohäsionsoperatorx ?? y
|
[9] | ||
| 2: Zuweisung und Verschiedenes | von rechts nach links |
Zuweisungx = y
|
[10] |
Additionszuweisungx += y
|
|||
Substraktionszuweisungx -= y
|
|||
Exponentiationszuweisungx **= y
|
|||
Multiplikationszuweisungx *= y
|
|||
Divisionszuweisungx /= y
|
|||
Restzuweisungx %= y
|
|||
Linksverschiebungszuweisungx <<= y
|
|||
Rechtsverschiebungszuweisungx >>= y
|
|||
Rechtsverschiebung-Ohne-Vorzeichen-Zuweisungx >>>= y
|
|||
Bitweise UND-Zuweisungx &= y
|
|||
Bitweise XOR-Zuweisungx ^= y
|
|||
Bitweise ODER-Zuweisungx |= y
|
|||
Logische UND-Zuweisungx &&= y
|
|||
Logische ODER-Zuweisungx ||= y
|
|||
Null-Kohäsionszuweisungx ??= y
|
|||
| von rechts nach links |
Bedingter (ternärer) Operatorx ? y : z
|
[11] | |
| von rechts nach links |
Pfeilx => y
|
[12] | |
| n/a | yield x |
||
yield* x |
|||
Spread...x
|
[13] | ||
| 1: Komma | von links nach rechts |
Kommmaoperatorx, y
|
Anmerkungen:
- Der Operand kann ein beliebiger Ausdruck sein.
- Die "rechte Seite" muss ein Bezeichner sein.
- Die "rechte Seite" kann ein beliebiger Ausdruck sein.
- Die "rechte Seite" ist eine kommaseparierte Liste von beliebigen Ausdrücken mit Präzedenz > 1 (d.h. keine Kommaaussdrücke). Der Konstruktor eines
new-Ausdrucks kann keine optionale Kette sein. - Der Operand muss ein gültiges Zuweisungsziel sein (Bezeichner oder Zugriff auf Eigenschaften). Seine Präzedenz bedeutet
new Foo++ist(new Foo)++(ein Syntaxfehler) und nichtnew (Foo++)(ein TypeError: (Foo++) ist kein Konstruktor). - Der Operand muss ein gültiges Zuweisungsziel sein (Bezeichner oder Zugriff auf Eigenschaften).
- Der Operand kann kein Bezeichner oder Zugriff auf ein privates Element sein.
- Die linke Seite kann keine Präzedenz von 14 haben.
- Die Operanden können keine logischen ODER
||oder logischen UND&&Operatoren ohne Gruppierung sein. - Die "linke Seite" muss ein gültiges Zuweisungsziel sein (Bezeichner oder Zugriff auf Eigenschaften).
- Die Assoziativität bedeutet, dass die beiden Ausdrücke nach
?implizit gruppiert sind. - Die "linke Seite" ist ein einzelner Bezeichner oder eine eingeklammerte Parameterliste.
- Nur gültig innerhalb von Objektliteralen, Arrayliteralen oder Argumentlisten.
Die Präzedenz der Gruppen 17 und 16 kann etwas zweideutig sein. Hier sind einige Beispiele zur Klarstellung.
- Optionales Chaining ist immer austauschbar mit seiner jeweiligen Syntax ohne Optionalität (mit Ausnahme von ein paar speziellen Fällen, in denen optionales Chaining verboten ist). Zum Beispiel akzeptiert jeder Ort, der
a?.bakzeptiert, aucha.bund umgekehrt, und ebenso füra?.(),a(), usw. - Memberausdrücke und berechnete Memberausdrücke sind immer austauschbar.
- Aufrufausdrücke und
import()-Ausdrücke sind immer austauschbar. - Dies lässt vier Klassen von Ausdrücken übrig: Zugriff auf Member,
newmit Argumenten, Funktionsaufruf undnewohne Argumente.- Die "linke Seite" eines Zugriffs auf Member kann sein: ein Zugriff auf Member (
a.b.c),newmit Argumenten (new a().b), und Funktionsaufruf (a().b). - Die "linke Seite" von
newmit Argumenten kann sein: ein Zugriff auf Member (new a.b()) undnewmit Argumenten (new new a()()). - Die "linke Seite" eines Funktionsaufrufs kann sein: ein Zugriff auf Member (
a.b()),newmit Argumenten (new a()()), und Funktionsaufruf (a()()). - Der Operand von
newohne Argumente kann sein: ein Zugriff auf Member (new a.b),newmit Argumenten (new new a()), undnewohne Argumente (new new a).
- Die "linke Seite" eines Zugriffs auf Member kann sein: ein Zugriff auf Member (