Operatorpräzedenz
Operatorpräzedenz bestimmt, wie Operatoren im Verhältnis zueinander geparst werden. Operatoren mit höherer Präzedenz werden die 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 OPerators 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 die Sprache annimmt, hängt von der Identität von OP1 und OP2 ab.
Haben OP1 und OP2 unterschiedliche Präzedenzstufen (siehe Tabelle unten), geht der Operator mit der höheren Präzedenz zuerst und die Assoziativität spielt keine Rolle. Beachten Sie, wie die Multiplikation höhere Präzedenz als die Addition hat und zuerst ausgeführt wird, obwohl die Addition im Code zuerst geschrieben ist.
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 diese nach der 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, so dass Sie schreiben können:
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 — der Rückgabewert von b = 5, also der rechte Operand der Zuweisung.
Ein weiteres Beispiel ist der einzigartige Exponentialoperator, der rechtsassoziativ ist, wohingegen andere arithmetische Operatoren linksassoziativ sind.
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 zuerst nach Präzedenz und dann bei benachbarten Operatoren mit derselben Präzedenz nach Assoziativität gruppiert. Bei der Mischung von Division und Exponentialoperation kommt die Exponentialoperation immer vor der Division. Zum Beispiel ergibt 2 ** 3 / 3 ** 2 0.8888888888888888, da es dasselbe ist wie (2 ** 3) / (3 ** 2).
Betrachten wir für unäre Präfixoperatoren folgendes Muster:
OP1 a OP2 b
wobei OP1 ein Präfix-Unäroperator und OP2 ein binärer Operator ist. Wenn OP1 eine höhere Präzedenz als OP2 hat, wird es als (OP1 a) OP2 b gruppiert; 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"
Befindet sich der Unäroperator am zweiten Operanden:
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. Zum Glück haben die meisten unären Operatoren eine höhere Präzedenz als binäre Operatoren und leiden nicht unter diesem Problem.
Haben wir zwei Präfix-Unäroperator:
OP1 OP2 a
Dann muss der unäre Operator, der näher am Operand steht, OP2, eine höhere Präzedenz als OP1 haben, damit es als OP1 (OP2 a) gruppiert wird. Es ist möglich, es anders 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 bedeutet, dass auf eine Kennung namens yield gewartet wird, und dies ist ein Syntaxfehler. Ebenso, wenn Sie new !A; haben, da ! eine niedrigere Präzedenz als new hat, würde dies zu (new !) A, was offensichtlich ungültig ist (dieser Code wäre ohnehin unsinnig zu schreiben, da !A immer einen booleschen Wert und keine Konstruktorfunktion erzeugt).
Für postfix Unäroperatoren (nämlich ++ und --) gelten dieselben Regeln. Zum Glück haben beide Operatoren eine höhere Präzedenz als jeder binäre Operator, so dass die Gruppierung immer wie erwartet ist. Außerdem, da ++ zu einem Wert und nicht zu einer Referenz evaluiert, können Sie keine mehrfachen Inkremente zusammenkettieren.
let a = 1;
a++++; // SyntaxError: Invalid left-hand side in postfix operation.
Die Operatorpräzedenz wird rekursiv behandelt. Betrachten Sie zum Beispiel diesen Ausdruck:
1 + 2 ** 3 * 4 / 5 >> 6
Zuerst gruppieren wir Operatoren mit unterschiedlichen Präzedenzstufen nach abnehmenden Präzedenzstufen.
- Der
**-Operator hat die höchste Präzedenz, daher wird er zuerst gruppiert. - Um den
**-Ausdruck herum befindet sich*rechts und+links.*hat eine höhere Präzedenz, daher wird es zuerst gruppiert.*und/haben die gleiche Präzedenz, daher gruppieren wir sie vorerst zusammen. - Um den in 2 gruppierten
*//-Ausdruck herum, da+eine höhere Präzedenz als>>hat, wird der erstere 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 die Operatorpräzedenz und Assoziativität nur die Ausführungsreihenfolge der Operatoren (die implizite Gruppierung) beeinflussen, aber nicht die Ausführungsreihenfolge der Operanden. Die Operanden werden immer von links nach rechts abgearbeitet. Die höher präzedenten Ausdrücke werden immer zuerst ausgewertet, und ihre Ergebnisse werden dann entsprechend 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 daran als an eine Post-Order-Durchlauf.
/
┌────────┴────────┐
echo("left", 4) **
┌────────┴────────┐
echo("middle", 3) echo("right", 2)
Nachdem alle Operatoren korrekt gruppiert wurden, würde sich für die binären Operatoren ein Binärbaum ergeben. Die Auswertung beginnt bei der äußersten Gruppe — das ist der Operator mit der niedrigsten Präzedenz (in diesem Fall /). Der linke Operand dieses Operators wird zuerst ausgewertet, was aus höherpräzedenten Operatoren bestehen kann (wie einem Aufrufausdruck echo("left", 4)). Nachdem der linke Operand ausgewertet wurde, wird der rechte Operand in der gleichen Weise ausgewertet. Daher werden alle Blattknoten — die echo()-Aufrufe — von links nach rechts besucht, unabhängig von der Präzedenz der Operatoren, die sie verbinden.
Kurzschlussverhalten
Im vorherigen Abschnitt haben wir gesagt, dass "die höher präzedenten Ausdrücke immer zuerst ausgewertet werden" — das ist im Allgemeinen richtig, muss jedoch mit der Erkenntnis des Kurzschlussverhaltens ergänzt werden, in welchem Fall ein Operand möglicherweise überhaupt nicht ausgewertet wird.
Kurzschlussverhalten ist ein Fachbegriff für bedingte 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 daher eine höhere Präzedenz als && hat. Wir könnten sagen, dass der logische UND-Operator (&&) "kurzgeschlossen" wird. Neben dem logischen UND gehören auch logisches ODER (||), Nullish-Koaleszenz (??) und optionales Chaining (?.) zu den kurzgeschlossenen Operatoren.
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 der linke Operand immer ausgewertet. Der rechte Operand wird nur ausgewertet, wenn der linke Operand das Ergebnis der Operation nicht bestimmen kann.
Hinweis:
Das Verhalten des Kurzschlusses ist in diese Operatoren eingebaut. 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 nie etwas anderes als NaN sein könnte.
Das vorherige Modell der Post-Order-Durchlauf bleibt bestehen. Allerdings entscheidet die Sprache, nach dem Besuch des linken Teilbaums eines kurzgeschlossenen Operators, ob der rechte Operand ausgewertet werden muss. Wenn nicht (zum Beispiel weil der linke Operand von || bereits truthy ist), wird das Ergebnis direkt zurückgegeben, ohne den rechten Teilbaum 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, trotz && höherer Präzedenz. Dies bedeutet nicht, dass || in diesem Fall eine höhere Präzedenz hat — es ist genau weil (B() && A()) eine höhere Präzedenz hat, dass es als Ganzes vernachlässigt wird. Wird es wie folgt neu angeordnet:
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() insgesamt false ist, würde C() immer noch ausgewertet.
Beachten Sie jedoch, dass Kurzschlussverhalten das endgültige Auswertungsergebnis nicht ändert. Es beeinflusst nur die Auswertung der Operanden, nicht wie Operatoren gruppiert werden — wenn die Auswertung der Operanden keine Nebeneffekte hat (zum Beispiel Ausgabe ins Konsolenprotokoll, Zuweisungen zu Variablen, Werfen eines Fehlers), wäre das Kurzschlussverhalten überhaupt nicht beobachtbar.
Die Zuweisungsgegenstücke dieser Operatoren (&&=, ||=, ??=) sind ebenfalls kurzgeschlossen. Sie sind so kurzgeschlossen, dass die Zuweisung überhaupt nicht stattfindet.
Tabelle
Die folgende Tabelle listet Operatoren in der Reihenfolge von höchster Präzedenz (18) bis niedrigster Präzedenz (1) auf.
Einige allgemeine Anmerkungen zur Tabelle:
- Nicht alle hier enthaltenen Syntaxen sind im strengen Sinne "Operatoren". Zum Beispiel werden Spread
...und Pfeil=>typischerweise nicht als Operatoren betrachtet. Wir haben sie jedoch dennoch aufgenommen, um zu zeigen, wie eng sie im Vergleich zu anderen Operatoren/Ausdrucken binden. - Einige Operatoren haben bestimmte Operanden, die Ausdrücke erfordern, die schmaler sind als jene, die von höheren Präzedenzoperatoren produziert werden. Zum Beispiel muss die rechte Seite des Mitgliederzugangs
.(Präzedenz 17) ein Bezeichner anstelle eines gruppierten Ausdrucks sein. Die linke Seite des Pfeils=>(Präzedenz 2) muss eine Argumentenliste oder ein einzelner Bezeichner anstelle eines zufälligen Ausdrucks sein. - Einige Operatoren haben bestimmte Operanden, die Ausdrücke akzeptieren, die weiter sind als jene, die von höheren Präzedenzoperatoren produziert werden. Zum Beispiel kann der klammerumfasste Ausdruck der Klammernotation
[ … ](Präzedenz 17) jeder Ausdruck sein, selbst durch Komma (Präzedenz 1) verbundene. Diese Operatoren wirken, als ob jener Operand "automatisch gruppiert" wäre. In diesem Fall werden wir die Assoziativität weglassen.
| Präzedenz | Assoziativität | Individuelle Operatoren | Anmerkungen |
|---|---|---|---|
| 18: Gruppierung | n/v | Grouping(x) |
[1] |
| 17: Zugriff und Aufruf | links-nach-rechts | Mitgliedszugriffx.y |
[2] |
Optionales Chainingx?.y |
|||
| n/v |
Berechneter Mitgliedszugriffx[y]
|
[3] | |
new mit Argumentenlistenew x(y) |
[4] | ||
Funktionsaufrufx(y)
|
|||
import(x) |
|||
| 16: new | n/v | new ohne Argumentenlistenew x |
|
| 15: Postfix-Operatoren | n/v |
Postfix-Inkrementx++
|
[5] |
Postfix-Dekrementx--
|
|||
| 14: Präfix-Operatoren | n/v |
Präfix-Inkrement++x
|
[6] |
Präfix-Dekrement--x
|
|||
Logisches NOT!x
|
|||
Bitweises NOT~x
|
|||
Unäres Plus+x
|
|||
Unäre Negation-x
|
|||
typeof x |
|||
void x |
|||
delete x |
[7] | ||
await x |
|||
| 13: Exponentialfunktion | rechts-nach-links |
Exponentialfunktionx ** y
|
[8] |
| 12: Multiplikative Operatoren | links-nach-rechts |
Multiplikationx * y
|
|
Divisionx / y
|
|||
Restx % y
|
|||
| 11: Additive Operatoren | links-nach-rechts |
Additionx + y
|
|
Subtraktionx - y
|
|||
| 10: Bitweise Verschiebung | links-nach-rechts |
Linksverschiebungx << y
|
|
Rechtsverschiebungx >> y
|
|||
Unsigned Rechtsverschiebungx >>> y
|
|||
| 9: Relationale Operatoren | links-nach-rechts |
Kleiner alsx < y
|
|
Kleiner als oder gleichx <= y
|
|||
Größer alsx > y
|
|||
Größer als oder gleichx >= y
|
|||
x in y |
|||
x instanceof y |
|||
| 8: Gleichheitsoperatoren | links-nach-rechts |
Gleichheitx == y
|
|
Ungleichheitx != y
|
|||
Strikte Gleichheitx === y
|
|||
Strikte Ungleichheitx !== y
|
|||
| 7: Bitweises UND | links-nach-rechts |
Bitweises UNDx & y
|
|
| 6: Bitweises XOR | links-nach-rechts |
Bitweises XORx ^ y
|
|
| 5: Bitweises ODER | links-nach-rechts |
Bitweises ODERx | y
|
|
| 4: Logisches UND | links-nach-rechts |
Logisches UNDx && y
|
|
| 3: Logisches ODER, Nullish-Koaleszenz | links-nach-rechts |
Logisches ODERx || y
|
|
Nullish-Koaleszenz-operatorx ?? y
|
[9] | ||
| 2: Zuweisung und Verschiedenes | rechts-nach-links |
Zuweisungx = y
|
[10] |
Additionszuweisungx += y
|
|||
Subtraktionszuweisungx -= y
|
|||
Exponentialzuweisungx **= y
|
|||
Multiplikationszuweisungx *= y
|
|||
Divisionszuweisungx /= y
|
|||
Restzuweisungx %= y
|
|||
Linksverschiebungszuweisungx <<= y
|
|||
Rechtsverschiebungszuweisungx >>= y
|
|||
Unsigned Rechtsverschiebungszuweisungx >>>= y
|
|||
Bitweise UND-Zuweisungx &= y
|
|||
Bitweise XOR-Zuweisungx ^= y
|
|||
Bitweise ODER-Zuweisungx |= y
|
|||
Logische UND-Zuweisungx &&= y
|
|||
Logische ODER-Zuweisungx ||= y
|
|||
Nullish-Koaleszenz-Zuweisungx ??= y
|
|||
| rechts-nach-links |
Bedingungsoperator (ternär)x ? y : z
|
[11] | |
| rechts-nach-links |
Pfeilx => y
|
[12] | |
| n/v | yield x |
||
yield* x |
|||
Spread...x
|
[13] | ||
| 1: Komma | links-nach-rechts |
Kommaoperatorx, y
|
Anmerkungen:
- Der Operand kann jeder Ausdruck sein.
- Die "rechte Seite" muss ein Bezeichner sein.
- Die "rechte Seite" kann jeder Ausdruck sein.
- Die "rechte Seite" ist eine durch Komma getrennte Liste beliebiger Ausdrücke mit Präzedenz > 1 (d.h. keine Kommaausdrücke). Der Konstruktor eines
new-Ausdrucks kann keine optionale Kette sein. - Der Operand muss ein gültiges Zuweisungsziel sein (Bezeichner oder Memberzugriff). Seine Präzedenz bedeutet, dass
new Foo++(new Foo)++(ein Syntaxfehler) und nichtnew (Foo++)(ein TypeError: (Foo++) ist kein Konstruktor) ist. - Der Operand muss ein gültiges Zuweisungsziel sein (Bezeichner oder Memberzugriff).
- Der Operand kann kein Bezeichner oder ein privates Element-Zugriff sein.
- Die linke Seite kann keine Präzedenz 14 haben.
- Die Operanden können nicht ein logisches ODER
||oder ein logisches UND&&Operator ohne Gruppierung sein. - Die "linke Seite" muss ein gültiges Zuweisungsziel sein (Bezeichner oder Memberzugriff).
- Die Assoziativität bedeutet, dass die beiden Ausdrücke nach
?implizit gruppiert sind. - Die "linke Seite" ist ein einzelner Bezeichner oder eine parenthesierte Parameterliste.
- Nur gültig innerhalb von Objektliteralen, Array-Literalen oder Argumentenlisten.
Die Präzedenz der Gruppen 17 und 16 kann etwas mehrdeutig sein. Hier sind einige Beispiele zur Klärung.
- Optionales Chaining ist immer für seine jeweilige Syntax ohne Optionalität austauschbar (außer in einigen speziellen Fällen, in denen optionales Chaining verboten ist). Zum Beispiel akzeptiert jeder Ort, der
a?.bakzeptiert, aucha.bund umgekehrt und ähnlich füra?.(),a(), etc. - Mitgliedsausdrücke und berechnete Mitgliedsausdrücke sind immer austauschbar.
- Aufrufausdrücke und
import()-Ausdrücke sind immer austauschbar. - Dies lässt vier Klassen von Ausdrücken: Mitgliedszugriff,
newmit Argumenten, Funktionsaufruf undnewohne Argumente.- Die "linke Seite" eines Mitgliedszugriffs kann sein: ein Mitgliedszugriff (
a.b.c),newmit Argumenten (new a().b) und Funktionsaufruf (a().b). - Die "linke Seite" von
newmit Argumenten kann sein: ein Mitgliedszugriff (new a.b()) undnewmit Argumenten (new new a()()). - Die "linke Seite" eines Funktionsaufrufs kann sein: ein Mitgliedszugriff (
a.b()),newmit Argumenten (new a()()), und Funktionsaufruf (a()()). - Der Operand von
newohne Argumente kann sein: ein Mitgliedszugriff (new a.b),newmit Argumenten (new new a()), undnewohne Argumente (new new a).
- Die "linke Seite" eines Mitgliedszugriffs kann sein: ein Mitgliedszugriff (