Operatorpräzedenz
Die Operatorpräzedenz bestimmt, wie Operatoren im Verhältnis zueinander geparst werden. Operatoren mit höherer Präzedenz werden zu 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 unten stehende Darstellung beschreibbar ist, wobei sowohl OP1
als auch OP2
Platzhalter für Operationen 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 davon 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), geht der Operator mit der höheren Präzedenz zuerst, und die Assoziativität spielt keine Rolle. Beachten Sie, wie Multiplikation eine höhere Präzedenz als Addition hat und zuerst ausgeführt wird, auch wenn Addition im Code zuerst geschrieben wird.
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
Bei Operatoren mit der gleichen Präzedenz werden diese durch Assoziativität gruppiert. 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, sodass Sie folgendes 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 zugewiesenen Wert zurückgibt. Zuerst wird b
auf 5 gesetzt. Dann wird auch a
auf 5 gesetzt – den Rückgabewert von b = 5
, also der rechte Operand der Zuweisung.
Ein weiteres Beispiel ist der eindeutige Exponentierungsoperator, der rechtsassoziativ ist, während 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 gruppiert und dann für benachbarte Operatoren mit gleicher Präzedenz nach Assoziativität. Beim Mischen von Division und Exponentiation kommt die Exponentierung immer vor der Division. Zum Beispiel ergibt 2 ** 3 / 3 ** 2
das Ergebnis 0.8888888888888888, da es dem Ausdruck (2 ** 3) / (3 ** 2)
entspricht.
Für unäre Präfixoperatoren nehmen wir folgendes Muster an:
OP1 a OP2 b
wobei OP1
ein unärer Präfixoperator und OP2
ein binärer Operator ist. Wenn OP1
eine höhere Präzedenz als OP2
hat, würde er als (OP1 a) OP2 b
gruppiert; andernfalls würde er als OP1 (a OP2 b)
gruppiert.
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 Operand liegt:
a OP2 OP1 b
Dann muss der binäre Operator OP2
eine niedrigere Präzedenz als der unäre Operator OP1
haben, damit er als a OP2 (OP1 b)
gruppiert wird. Zum Beispiel ist folgendes 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 weil 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 unäre Präfixoperatoren haben:
OP1 OP2 a
Dann muss der unäre Operator, der näher am Operanden ist, OP2
, eine höhere Präzedenz als OP1
haben, damit er als OP1 (OP2 a)
gruppiert wird. Es ist möglich, es andersherum 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
ausgewertet wird, und ein Syntaxfehler. Ebenso, wenn Sie new !A;
haben, da !
eine niedrigere Präzedenz als new
hat, würde dies zu (new !) A
werden, was offenkundig ungültig ist. (Dieser Code wäre sowieso unsinnig zu schreiben, da !A
immer ein Boolean ergibt und keine Konstruktorfunktion.)
Für unäre Postfix-Operatoren (nämlich ++
und --
) gelten die gleichen Regeln. Glücklicherweise haben beide Operatoren eine höhere Präzedenz als jeder binäre Operator, sodass die Gruppierung immer wie erwartet ist. Zudem ergibt ++
einen Wert und keinen Referenz, sodass Sie keine mehrfachen Inkremente wie in C anketten können.
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 unterschiedlicher Präzedenz nach abnehmenden Präzedenzebenen.
- Der
**
-Operator hat die höchste Präzedenz, also wird er zuerst gruppiert. - Um den
**
-Ausdruck herum hat er*
auf der rechten Seite und+
auf der linken.*
hat eine höhere Präzedenz, also wird er zuerst gruppiert.*
und/
haben die gleiche Präzedenz, sodass sie zunächst zusammen gruppiert werden. - Um den in 2 gruppierten
*
//
-Ausdruck herum wird+
zuerst gruppiert, da es eine höhere Präzedenz als>>
hat.
(1 + ( (2 ** 3) * 4 / 5) ) >> 6
// │ │ └─ 1. ─┘ │ │
// │ └────── 2. ───────┘ │
// └────────── 3. ──────────┘
Innerhalb der *
//
-Gruppe, da sie beide linksassoziativ sind, würde 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 Reihenfolge der Auswertung von Operatoren betrifft (die implizite Gruppierung), aber nicht die Reihenfolge der Auswertung von Operanden. Die Operanden werden immer von links nach rechts ausgewertet. Die höher-priorisierten Ausdrücke 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 daran als eine Post-Order Traversion.
/ ┌────────┴────────┐ echo("left", 4) ** ┌────────┴────────┐ echo("middle", 3) echo("right", 2)
Nachdem alle Operatoren korrekt gruppiert sind, würden die binären Operatoren einen Binärbaum bilden. 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öher-priorisierten Operatoren (wie einem Aufrufausdruck echo("left", 4)
) bestehen kann. Nachdem der linke Operand ausgewertet wurde, wird der rechte Operand auf die gleiche Weise ausgewertet. Daher würden alle Blattknoten – die echo()
-Aufrufe – unabhängig von der Präzedenz der diese verbindenden Operatoren von links nach rechts besucht werden.
Short-Circuiting
Im vorherigen Abschnitt haben wir gesagt, "die höher-priorisierten Ausdrücke werden immer zuerst ausgewertet" – dies ist im Allgemeinen wahr, muss aber mit der Anerkennung des Short-Circuiting ergänzt werden, bei dem ein Operand möglicherweise gar nicht ausgewertet wird.
Short-Circuiting ist ein Fachausdruck für bedingte Auswertung. Beispielsweise wird im Ausdruck a && (b + c)
der Subausdruck (b + c)
nicht einmal ausgewertet, wenn a
falsy ist, selbst wenn er gruppiert ist und daher eine höhere Präzedenz als &&
hat. Wir könnten sagen, dass der logische UND-Operator (&&
) "short-circuited" ist. Zusammen mit logischem UND sind andere short-circuiting Operatoren logisches ODER (||
), Null-Koaleszenz (??
) und optionales 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 short-circuiting Operators wird der linke Operand immer ausgewertet. Der rechte Operand wird nur dann ausgewertet, wenn der linke Operand das Ergebnis der Operation nicht bestimmen kann.
Hinweis:
Das Verhalten des Short-Circuiting ist in diesen Operatoren verankert. Andere Operatoren würden immer beide Operanden auswerten, unabhängig davon, ob dies tatsächlich nützlich ist – zum Beispiel wird NaN * foo()
immer foo
aufrufen, selbst wenn das Ergebnis nie etwas anderes als NaN
wäre.
Das vorherige Modell einer Post-Order Traversion bleibt bestehen. Nachdem der linke Teilbaum eines short-circuiting Operators besucht wurde, entscheidet die Sprache, ob der rechte Operand ausgewertet werden muss. Falls nicht (z.B. weil der linke Operand von ||
bereits wahrheitsgemäß ist), wird das Ergebnis ohne Besuch des rechten Teilbaums direkt zurückgegeben.
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 – es ist genau weil (B() && A())
eine höhere Präzedenz hat, dass es als Ganzes vernachlässigt wird. Wenn es folgendermaßen neu angeordnet wird:
console.log(A() && C() || B());
// Logs:
// called A
// called B
// false
Dann würde der short-circuiting Effekt von &&
nur verhindern, dass C()
ausgewertet wird, aber da A() && C()
als Ganzes false
ist, würde B()
immer noch ausgewertet werden.
Beachten Sie jedoch, dass Short-Circuiting das endgültige Auswertungsergebnis nicht ändert. Es beeinflusst nur die Auswertung von Operanden, nicht wie Operatoren gruppiert werden – wenn die Auswertung von Operanden keine Seiteneffekte hat (z.B. Konsolenausgabe, Zuweisungen an Variablen, Auslösen eines Fehlers), wäre Short-Circuiting überhaupt nicht beobachtbar.
Die Zuweisungsgegenstücke dieser Operatoren (&&=
, ||=
, ??=
) sind ebenfalls short-circuiting. Sie sind auf eine Weise short-circuiting, dass die Zuweisung überhaupt nicht stattfindet.
Tabelle
Die folgende Tabelle listet Operatoren in der Reihenfolge von höchster Präzedenz (18) zu niedrigster Präzedenz (1) auf.
Einige allgemeine Anmerkungen zur Tabelle:
- Nicht alle hier enthaltenen Syntaxelemente sind im strengen Sinne „Operatoren“. Zum Beispiel werden Spread
...
und Pfeil=>
normalerweise nicht als Operatoren angesehen. Wir haben sie jedoch aufgenommen, um zu zeigen, wie fest sie im Vergleich zu anderen Operatoren/Anweisungen binden. - Einige Operatoren haben bestimmte Operanden, die Ausdrücke erfordern, die enger sind als diejenigen, die von Operatoren mit höherer Präzedenz erzeugt werden. Zum Beispiel muss die rechte Seite der Member-Zugriff
.
(Präzedenz 17) ein Bezeichner statt eines gruppierten Ausdrucks sein. Die linke Seite des Pfeils=>
(Präzedenz 2) muss eine Argumentliste oder ein einzelner Bezeichner statt eines beliebigen Ausdrucks sein. - Einige Operatoren haben bestimmte Operanden, die Ausdrücke akzeptieren, die breiter sind als diejenigen, die von Operatoren mit höherer Präzedenz erzeugt werden. Das in Klammern eingeschlossene Ausdruck des Klammerausdrucks
[ … ]
(Präzedenz 17) kann jeder Ausdruck sein, auch durch Komma (Präzedenz 1) verbundene. Diese Operatoren handeln so, 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 | Member accessx.y |
[2] |
Optional chainingx?.y |
|||
n/a |
Computed member accessx[y]
|
[3] | |
new mit Argumentlistenew x(y) |
[4] | ||
Funktionsaufrufx(y)
|
|||
import(x) |
|||
16: new | n/a | new ohne Argumentlistenew x |
|
15: Postfix-Operatoren | n/a |
Postfix-inkrementx++
|
[5] |
Postfix-dekrementx--
|
|||
14: Präfix-Operatoren | n/a |
Präfix-inkrement++x
|
[6] |
Präfix-dekrement--x
|
|||
Logisches NICHT!x
|
|||
Bitweises NICHT~x
|
|||
Unäres Plus+x
|
|||
Unäre Negation-x
|
|||
typeof x |
|||
void x |
|||
delete x |
[7] | ||
await x |
|||
13: Exponentiation | von rechts nach links |
Exponentiationx ** y
|
[8] |
12: Multiplikative Operatoren | von links nach rechts |
Multiplikationx * y
|
|
Divisionx / y
|
|||
Restx % y
|
|||
11: Additive Operatoren | von links nach rechts |
Additionx + y
|
|
Subtraktionx - y
|
|||
10: Bitweiser Shift | von links nach rechts |
Linksshiftx << y
|
|
Rechtsshiftx >> y
|
|||
Unsigned Rechtsshiftx >>> y
|
|||
9: Relationale Operatoren | von links nach rechts |
Kleiner alsx < y
|
|
Kleiner oder gleichx <= y
|
|||
Größer alsx > y
|
|||
Größer 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-Koaleszenz | von links nach rechts |
Logisches ODERx || y
|
|
Null-Koaleszenzoperatorx ?? y
|
[9] | ||
2: Zuweisung und Verschiedenes | von rechts nach links |
Zuweisungx = y
|
[10] |
Addition-Zuweisungx += y
|
|||
Subtraktion-Zuweisungx -= y
|
|||
Exponentiation-Zuweisungx **= y
|
|||
Multiplikation-Zuweisungx *= y
|
|||
Division-Zuweisungx /= y
|
|||
Rest-Zuweisungx %= y
|
|||
Links-Shift-Zuweisungx <<= y
|
|||
Rechts-Shift-Zuweisungx >>= y
|
|||
Unsigned Rechts-Shift-Zuweisungx >>>= y
|
|||
Bitweises UND-Zuweisungx &= y
|
|||
Bitweises XOR-Zuweisungx ^= y
|
|||
Bitweises ODER-Zuweisungx |= y
|
|||
Logisches UND-Zuweisungx &&= y
|
|||
Logisches ODER-Zuweisungx ||= y
|
|||
Null-Koaleszenz-Zuweisungx ??= y
|
|||
von rechts nach links |
Bedingter (ternärer) Operatorx ? y : z
|
[11] | |
von rechts nach links |
Arrowx => y
|
[12] | |
n/a | yield x |
||
yield* x |
|||
Spread...x
|
[13] | ||
1: Komma | von links nach rechts |
Komma-Operatorx, 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 durch Komma getrennte Liste von Ausdrücken mit Präzedenz > 1 (d.h. keine Kommaausdrücke). Der Konstruktor eines
new
-Ausdrucks kann keine optionale Verkettung sein. - Der Operand muss ein gültiges Ziel einer Zuweisung sein (Bezeichner oder Zugriff auf eine Eigenschaft). 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 Ziel einer Zuweisung sein (Bezeichner oder Zugriff auf eine Eigenschaft).
- Der Operand kann kein Bezeichner oder ein Zugriff auf ein privates Element sein.
- Die linke Seite kann keine Präzedenz 14 haben.
- Die Operanden können kein logisches ODER
||
oder logisches UND&&
Operator ohne Gruppierung sein. - Die "linke Seite" muss ein gültiges Ziel einer Zuweisung sein (Bezeichner oder Zugriff auf eine Eigenschaft).
- Die Assoziativität bedeutet, dass die beiden Ausdrücke nach
?
implizit gruppiert sind. - Die "linke Seite" ist ein einzelner Bezeichner oder eine geklammerte Parameterliste.
- Gültig nur innerhalb von Objektsyntaxen, Arraysyntaxen oder Argumentlisten.
Die Präzedenz der Gruppen 17 und 16 kann etwas unklar sein. Hier sind ein paar Beispiele zur Klärung:
- Optionales Chaining ist immer austauschbar mit seiner jeweiligen Syntax ohne Optionalität (mit Ausnahme einiger weniger Fälle, in denen optionales Chaining verboten ist). Zum Beispiel kann überall, wo
a?.b
akzeptiert wird, aucha.b
akzeptiert werden und umgekehrt, und ähnlich füra?.()
,a()
, usw. - Memberausdrücke und berechnete Memberausdrücke sind immer gegeneinander austauschbar.
- Aufrufausdrücke und
import()
-Ausdrücke sind immer gegeneinander austauschbar. - Dies lässt vier Klassen von Ausdrücken: Memberzugriff,
new
mit Argumenten, Funktionsaufruf undnew
ohne Argumente.- Die "linke Seite" eines Memberzugriffs kann sein: ein Memberzugriff (
a.b.c
),new
mit Argumenten (new a().b
) und Funktionsaufruf (a().b
). - Die "linke Seite" von
new
mit Argumenten kann sein: ein Memberzugriff (new a.b()
) undnew
mit Argumenten (new new a()()
). - Die "linke Seite" eines Funktionsaufrufs kann sein: ein Memberzugriff (
a.b()
),new
mit Argumenten (new a()()
) und Funktionsaufruf (a()()
). - Der Operand von
new
ohne Argumente kann sein: ein Memberzugriff (new a.b
),new
mit Argumenten (new new a()
), undnew
ohne Argumente (new new a
).
- Die "linke Seite" eines Memberzugriffs kann sein: ein Memberzugriff (