Operatorpräzedenz
Operatorpräzedenz bestimmt, wie Operatoren im Verhältnis zueinander analysiert 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 folgende Darstellung beschrieben werden kann, 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 der Sprache bevorzugt wird, 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, dass die Multiplikation eine höhere Präzedenz hat als die Addition und zuerst ausgeführt wird, obwohl die Addition zuerst im Code 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 nach Assoziativität. Linksassozziativitä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 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 ist, weil der Zuweisungsoperator den zugewiesenen Wert zurückgibt. Zuerst wird b
auf 5 gesetzt. Dann wird auch a
auf 5 gesetzt — der Rückgabewert von b = 5
, also der rechte Operand der Zuweisung.
Als weiteres Beispiel hat der einzigartige Exponentialoperator Rechtsassoziativität, während andere arithmetische Operatoren 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 zuerst nach Präzedenz gruppiert und dann, für benachbarte Operatoren mit derselben Präzedenz, nach Assoziativität. Wenn man Division und Exponentialoperation mischt, kommt die Exponentialoperation immer vor der Division. Zum Beispiel ergibt 2 ** 3 / 3 ** 2
0.8888888888888888, weil es dasselbe ist wie (2 ** 3) / (3 ** 2)
.
Für Präfix-Unär-Operatoren nehmen wir an, wir haben das folgende Muster:
OP1 a OP2 b
wo OP1
ein Präfix-Unär-Operator und OP2
ein Binäroperator 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"
Wenn der Unär-Operator am zweiten Operand ist:
a OP2 OP1 b
Dann muss der Binäroperator OP2
eine niedrigere Präzedenz als der Unäroperator OP1
haben, damit es als a OP2 (OP1 b)
gruppiert wird. Zum Beispiel wäre das Folgende ungültig:
function* foo() {
a + yield 1;
}
Weil +
eine höhere Präzedenz als yield
hat, würde dies zu (a + yield) 1
werden — aber da yield
ein reserviertes Wort in Generator-Funktionen ist, wäre dies ein Syntaxfehler. Glücklicherweise haben die meisten Unäroperatoren eine höhere Präzedenz als Binäroperatoren und leiden nicht unter diesem Fallstrick.
Wenn wir zwei Präfix-Unär-Operatoren haben:
OP1 OP2 a
Dann muss der Unäroperator näher am Operand, OP2
, eine höhere Präzedenz als OP1
haben, damit es 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;
}
Weil await
eine höhere Präzedenz als yield
hat, würde dies zu (await yield) 1
führen, was auf ein Identifizierer yield
wartet und somit ein Syntaxfehler ist. Ähnlich, wenn Sie new !A;
haben, weil !
eine niedrigere Präzedenz als new
hat, würde es zu (new !) A
werden, was offensichtlich ungültig ist. (Dieser Code sieht sowieso unsinnig aus, da !A
immer ein boolescher Wert ist, keine Konstrukturfunktion.)
Für Nach-Unär-Operatoren (nämlich ++
und --
) gelten dieselben Regeln. Glücklicherweise haben beide Operatoren eine höhere Präzedenz als jeder Binäroperator, sodass die Gruppierung immer so erfolgt, wie Sie es erwarten. Außerdem, weil ++
zu einem Wert und nicht zu einer Referenz evaluiert, können Sie auch nicht mehrere Inkremente miteinander verketten, wie Sie es in C tun könnten.
let a = 1;
a++++; // SyntaxError: Invalid left-hand side in postfix operation.
Operatorpräzedenz wird rekursiv behandelt. Zum Beispiel, betrachten Sie diesen Ausdruck:
1 + 2 ** 3 * 4 / 5 >> 6
Zuerst gruppieren wir Operatoren mit unterschiedlicher Präzedenz in absteigender Reihenfolge der Präzedenz.
- Der
**
-Operator hat die höchste Präzedenz, also wird er zuerst gruppiert. - Um den
**
-Ausdruck herum hat er*
rechts und+
links.*
hat eine höhere Präzedenz, also wird es zuerst gruppiert.*
und/
haben die gleiche Präzedenz, also gruppieren wir sie vorerst miteinander. - Um den in 2 gruppierten
*
//
-Ausdruck herum wird da+
höher Präzedenz hat als>>
, ersteres gruppiert.
(1 + ( (2 ** 3) * 4 / 5) ) >> 6
// │ │ └─ 1. ─┘ │ │
// │ └────── 2. ───────┘ │
// └────────── 3. ──────────┘
Innerhalb der *
//
-Gruppe werden sie, weil sie beide linksassoziativ sind, auf der linken Seite gruppiert.
(1 + ( ( (2 ** 3) * 4 ) / 5) ) >> 6
// │ │ │ └─ 1. ─┘ │ │ │
// │ └─│─────── 2. ───│────┘ │
// └──────│───── 3. ─────│──────┘
// └───── 4. ─────┘
Beachten Sie, dass die Operatorpräzedenz und -assoziativität nur die Reihenfolge der Operatoren (die implizite Gruppierung), aber nicht die Reihenfolge der Operanden betrifft. Die Operanden werden immer von links nach rechts ausgewertet. Die höher-präzedenten Ausdrücke werden immer zuerst ausgewertet und ihre Ergebnisse 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ären Bäumen vertraut sind, denken Sie daran als eine Post-Order-Traversierung.
/ ┌────────┴────────┐ echo("left", 4) ** ┌────────┴────────┐ echo("middle", 3) echo("right", 2)
Nachdem alle Operatoren korrekt gruppiert sind, würden die Binäroperatoren einen Binärbaum bilden. Die Bewertung beginnt bei der äußersten Gruppe – die den Operator mit der niedrigsten Präzedenz hat (/
in diesem Fall). Der linke Operand dieses Operators wird zuerst ausgewertet, was aus höher-präzedenten Operatoren bestehen kann (wie ein Aufrufausdruck echo("left", 4)
). Nachdem der linke Operand ausgewertet wurde, wird der rechte Operand auf die gleiche Weise ausgewertet. Daher würden alle Blätter – die echo()
-Aufrufe – von links nach rechts besucht, unabhängig von der Präzedenz der Operatoren, die sie miteinander verbinden.
Kurzschluss
Im vorherigen Abschnitt sagten wir, "die höher-präzedenten Ausdrücke werden immer zuerst ausgewertet" — dies trifft im Allgemeinen zu, muss jedoch mit der Anerkennung des Kurzschlusses ergänzt werden, bei dem ein Operand möglicherweise überhaupt nicht ausgewertet wird.
Kurzschluss 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, obwohl er gruppiert ist und daher eine höhere Präzedenz als &&
hat. Man könnte sagen, dass der logische Und-Operator (&&
) "kurzgeschlossen" ist. Neben dem logischen Und sind andere kurzgeschlossene Operatoren logisches Oder (||
), Nullish-Koaleszenz (??
) und optionale Verkettung (?.
).
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`
Beim Auswerten 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 diesen 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, auch wenn das Ergebnis niemals etwas anderes als NaN
sein würde.
Das vorherige Modell einer Post-Order-Traversierung bleibt bestehen. Allerdings entscheidet die Sprache nach dem Besuch des linken Teilbaums eines kurzgeschlossenen Operators, ob der rechte Operand ausgewertet werden muss. Falls nicht (zum Beispiel, weil der linke Operand von ||
bereits wahrheitsgemäß 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, 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, die es dazu bringt, als Ganzes vernachlässigt zu werden. Wenn es umgestellt wird als:
console.log(A() && C() || B());
// Logs:
// called A
// called B
// false
Dann würde der Kurzschlusseffekt von &&
nur verhindern, dass C()
ausgewertet wird, aber weil A() && C()
als Ganzes false
ist, würde B()
dennoch ausgewertet.
Beachten Sie jedoch, dass Kurzschluss das endgültige Auswertungsergebnis nicht ändert. Es beeinflusst nur die Auswertung der Operanden, nicht wie Operatoren gruppiert sind — wenn die Auswertung von Operanden keine Nebenwirkungen hat (zum Beispiel, Protokollierung in die Konsole, Zuweisung zu Variablen, Fehler werfen), wäre der Kurzschluss ü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) zu niedrigster Präzedenz (1) auf.
Mehrere allgemeine Anmerkungen zur Tabelle:
- Nicht alle hier eingeschlossenen Syntaxen sind im strengen Sinne "Operatoren". Zum Beispiel werden Spread
...
und Pfeil=>
normalerweise nicht als Operatoren angesehen. Wir haben sie jedoch dennoch aufgenommen, um zu zeigen, wie eng sie im Vergleich zu anderen Operatoren/Ausdrücken binden. - Einige Operatoren haben bestimmte Operanden, die Ausdrücke schmaler erfordern als die von höher-präzedenten Operatoren erzeugten. Zum Beispiel muss die rechte Seite des Memberzugriffs
.
(Präzedenz 17) ein Identifikator anstelle eines gruppierten Ausdrucks sein. Die linke Seite des Pfeils=>
(Präzedenz 2) muss eine Liste von Argumenten oder ein einzelner Identifikator und nicht irgendein zufälliger Ausdruck sein. - Einige Operatoren haben bestimmte Operanden, die Ausdrücke akzeptieren, die breiter sind als die von höher-präzedenten Operatoren erzeugten. Zum Beispiel kann der in Klammern geschlossene Ausdruck der Klammernnotation
[ … ]
(Präzedenz 17) jeder Ausdruck sein, sogar durch Komma (Präzedenz 1) verbundene. Diese Operatoren agieren so, als wäre dieser Operand "automatisch gruppiert". In diesem Fall werden wir die Assoziativität auslassen.
Präzedenz | Assoziativität | Einzelne Operatoren | Anmerkungen |
---|---|---|---|
18: Gruppierung | n/a | Grouping(x) |
[1] |
17: Zugriff und Aufruf | von links nach rechts | Memberzugriffx.y |
[2] |
Optionale Verkettungx?.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: 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: Exponentialrechnung | von rechts nach links |
Exponentialrechnungx ** 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: Bitverschiebung | von links nach rechts |
Linksverschiebungx << y
|
|
Rechtsverschiebungx >> y
|
|||
Unsigned Rechtsverschiebungx >>> 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
|
|||
Strenge Gleichheitx === y
|
|||
Strenge 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, Nullish-Koaleszenz | von links nach rechts |
Logisches ODERx || y
|
|
Nullish-Coalescing-Operatorx ?? y
|
[9] | ||
2: Zuweisung und Verschiedenes | von rechts nach links |
Zuweisungx = y
|
[10] |
Additionszuweisungx += y
|
|||
Subtraktionszuweisungx -= y
|
|||
Exponentialzuweisungx **= y
|
|||
Multiplikationszuweisungx *= y
|
|||
Divisionszuweisungx /= y
|
|||
Restszuweisungx %= 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-Coalescing-Zuweisungx ??= y
|
|||
von rechts nach links |
Bedingungsoperator (ternär)x ? 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 |
Komma-Operatorx, y
|
Anmerkungen:
- Der Operand kann jeder Ausdruck sein.
- Die "rechte Seite" muss ein Identifikator sein.
- Die "rechte Seite" kann jeder Ausdruck sein.
- Die "rechte Seite" ist eine komma-separierte Liste von beliebigen Ausdrücken mit einer Präzedenz > 1 (d.h. keine Komma-Ausdrücke). Der Konstruktor eines
new
-Ausdrucks kann keine optionale Verkettung sein. - Der Operand muss ein gültiges Zuweisungsziel sein (Identifikator oder Memberzugriff). 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 (Identifikator oder Memberzugriff).
- Der Operand kann kein Identifikator oder privates Element-Zugriff sein.
- Die linke Seite kann keine Präzedenz 14 haben.
- Die Operanden dürfen kein logisches ODER
||
oder logischer UND&&
Operator ohne Gruppierung sein. - Die "linke Seite" muss ein gültiges Zuweisungsziel sein (Identifikator oder Memberzugriff).
- Die Assoziativität bedeutet, dass die beiden Ausdrücke nach
?
implizit gruppiert sind. - Die "linke Seite" ist ein einzelner Identifikator oder eine in Klammern gesetzte Parameterliste.
- Nur innerhalb von Objektliteralen, Array-Literalen oder Argumentlisten gültig.
Die Präzedenz der Gruppen 17 und 16 kann etwas zweideutig sein. Hier sind einige Beispiele zur Klärung:
- Optionale Verkettung ist immer durch ihre jeweilige Syntax ohne Optionalität ersetzbar (abgesehen von einigen besonderen Fällen, in denen optionale Verkettung verboten ist). Zum Beispiel kann jedes Vorkommen von
a?.b
aucha.b
akzeptieren und umgekehrt, und ähnlich füra?.()
,a()
, usw. - Memberausdrücke und berechnete Memberausdrücke sind immer füreinander austauschbar.
- Aufrufausdrücke und
import()
-Ausdrücke sind immer füreinander austauschbar. - Damit verbleiben 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 (