Operatorpriorität
Operatorpriorität bestimmt, wie Operatoren in Bezug aufeinander analysiert werden. Operatoren mit höherer Priorität werden zu den Operanden von Operatoren mit niedrigerer Priorität.
Probieren Sie es aus
Priorität 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 obenstehende Kombination erlaubt zwei mögliche Interpretationen:
(a OP1 b) OP2 c a OP1 (b OP2 c)
Welche Interpretation die Sprache annimmt, hängt von der Identität von OP1
und OP2
ab.
Wenn OP1
und OP2
unterschiedliche Prioritätsstufen haben (siehe Tabelle unten), wird der Operator mit der höheren Priorität zuerst ausgeführt und die Assoziativität spielt keine Rolle. Beachten Sie, wie die Multiplikation höhere Priorität als die Addition hat und zuerst ausgeführt wird, auch wenn 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 gleicher Priorität 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
auch auf 5 gesetzt — der Rückgabewert von b = 5
, also der rechte Operand der Zuweisung.
Ein weiteres Beispiel: Der einzigartige Potenzierungsoperator hat Rechtsassoziativität, 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 Priorität gruppiert und dann, bei benachbarten Operatoren mit derselben Priorität, nach Assoziativität. Beim Mischen von Division und Potenzierung kommt die Potenzierung immer vor der Division. Zum Beispiel ergibt 2 ** 3 / 3 ** 2
0,8888888888888888, weil es dasselbe ist wie (2 ** 3) / (3 ** 2)
.
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 Priorität als OP2
hat, dann wird es als (OP1 a) OP2 b
gruppiert; andernfalls würde es OP1 (a OP2 b)
sein.
const a = 1;
const b = 2;
typeof a + b; // Equivalent to (typeof a) + b; result is "number2"
Wenn der unäre Operator am zweiten Operand ist:
a OP2 OP1 b
Dann muss der binäre Operator OP2
eine niedrigere Priorität als der unäre Operator OP1
haben, damit es als a OP2 (OP1 b)
gruppiert wird. Ein Beispiel dafür ist ungültig:
function* foo() {
a + yield 1;
}
Da +
eine höhere Priorität als yield
hat, würde daraus (a + yield) 1
werden — da aber yield
ein reserviertes Wort in Generatorfunktionen ist, wäre das ein Syntaxfehler. Glücklicherweise haben die meisten unären Operatoren eine höhere Priorität als binäre Operatoren und leiden nicht unter dieser Falle.
Wenn wir zwei unäre Präfixoperatoren haben:
OP1 OP2 a
Dann muss der unäre Operator näher am Operanden, OP2
, eine höhere Priorität 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;
}
Da await
eine höhere Priorität als yield
hat, würde daraus (await yield) 1
werden, was bedeutet, dass auf einen Bezeichner namens yield
gewartet wird, was ein Syntaxfehler ist. Ebenso, wenn Sie new !A;
haben, würde wegen der niedrigeren Priorität von !
gegenüber new
daraus (new !) A
, was offensichtlich ungültig ist. (Dieser Code scheint sowieso unsinnig zu sein, da !A
immer ein Boolean ergibt, nicht eine Konstruktorfunktion.)
Für unäre Postfixoperatoren (nämlich ++
und --
) gelten dieselben Regeln. Glücklicherweise haben beide Operatoren eine höhere Priorität als jeder binäre Operator, sodass die Gruppierung immer wie erwartet ausfällt. Zudem, da ++
zu einem Wert evaluiert wird, nicht einer Referenz, können Sie nicht mehrere Inkremente nacheinander verketten, wie Sie es in C tun könnten.
let a = 1;
a++++; // SyntaxError: Invalid left-hand side in postfix operation.
Die Operatorpriorität wird rekursiv gehandhabt. Betrachten wir zum Beispiel diesen Ausdruck:
1 + 2 ** 3 * 4 / 5 >> 6
Zuerst gruppieren wir die Operatoren mit unterschiedlicher Priorität nach absteigenden Prioritätsstufen.
- Der
**
Operator hat die höchste Priorität, daher wird er zuerst gruppiert. - Um den
**
Ausdruck herum befindet sich*
rechts und+
links.*
hat eine höhere Priorität, daher wird es zuerst gruppiert.*
und/
haben dieselbe Priorität, daher werden sie vorerst zusammengefasst. - Um den in 2 gruppierten
*
//
Ausdruck herum, wird+
aufgrund der höheren Priorität gegenüber>>
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 Operatorpriorität und -assoziativität nur die Reihenfolge der Auswertung von Operatoren (die implizite Gruppierung) beeinflussen, nicht jedoch 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 entsprechend der Reihenfolge der Operatorprioritäten 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 post-order traversal.
/ ┌────────┴────────┐ echo("left", 4) ** ┌────────┴────────┐ echo("middle", 3) echo("right", 2)
Nachdem alle Operatoren korrekt gruppiert wurden, würden die binären Operatoren einen binären Baum bilden. Die Auswertung beginnt mit der äußersten Gruppe — die der Operator mit der niedrigsten Priorität (in diesem Fall /
). Der linke Operand dieses Operators wird zuerst ausgewertet, was aus höher priorisierten Operatoren (wie einem Funktionsaufruf echo("left", 4)
) bestehen kann. Nachdem der linke Operand ausgewertet wurde, wird der rechte Operand auf die gleiche Weise ausgewertet. Daher werden alle Blattknoten — die echo()
-Aufrufe — von links nach rechts besucht, unabhängig von der Priorität der sie verbindenden Operatoren.
Kurzschlussauswertung
Im vorherigen Abschnitt sagten wir, "die höher priorisierten Ausdrücke werden immer zuerst ausgewertet" — dies ist im Allgemeinen wahr, muss jedoch mit der Anerkennung der Kurzschlussauswertung ergänzt werden, bei der ein Operand möglicherweise überhaupt nicht ausgewertet wird.
Kurzschlussauswertung ist Fachjargon für bedingte Auswertung. Zum Beispiel wird im Ausdruck a && (b + c)
, wenn a
falsch ist, der Unterausdruck (b + c)
nicht ausgewertet, selbst wenn er gruppiert ist und deshalb eine höhere Priorität als &&
hat. Wir könnten sagen, dass der logische UND-Operator (&&
) "gekürzt" ist. Neben dem logischen UND gehören zu den anderen gekürzten Operatoren logisches ODER (||
), Nullish Coalescing (??
) und Optional 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 gekürzten 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 nie etwas anderes als NaN
wäre.
Das vorherige Modell einer post-order Traversierung gilt nach wie vor. Jedoch wird, nachdem der linke Teilbaum eines gekürzten Operators besucht wurde, die Sprache entscheiden, ob der rechte Operand ausgewertet werden muss. Wenn nicht (zum Beispiel, weil der linke Operand von ||
bereits wahrhaftig 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 Priorität hat. Das bedeutet nicht, dass ||
in diesem Fall eine höhere Priorität hat — es ist genau wegen (B() && A())
mit höherer Priorität, dass es als Ganzes ignoriert wird. Wenn es umgestellt wird wie:
console.log(A() && C() || B());
// Logs:
// called A
// called B
// false
Dann würde der Kurzschluss-Effekt von &&
nur verhindern, dass C()
ausgewertet wird, aber da A() && C()
insgesamt false
ist, würde B()
dennoch ausgewertet.
Beachten Sie jedoch, dass die Kurzschlussauswertung das endgültige Auswertungsergebnis nicht ändert. Sie beeinflusst nur die Auswertung der Operanden, nicht die Gruppierung der Operatoren — wenn die Auswertung von Operanden keine Nebeneffekte hat (zum Beispiel Ausgabe auf die Konsole, Zuweisung an Variablen, Auslösen eines Fehlers), wäre die Kurzschlussauswertung überhaupt nicht beobachtbar.
Die Zuweisungsäquivalente dieser Operatoren (&&=
, ||=
, ??=
) sind ebenfalls gekürzt. Sie sind auf eine Weise gekürzt, dass die Zuweisung überhaupt nicht stattfindet.
Tabelle
Die folgende Tabelle listet Operatoren von der höchsten Priorität (18) bis zur niedrigsten Priorität (1) auf.
Einige allgemeine Anmerkungen zur Tabelle:
- Nicht alle hier eingeschlossenen Syntaxen sind im strengen Sinne "Operatoren". Zum Beispiel werden Spread
...
und Arrow=>
typischerweise nicht als Operatoren angesehen. Wir haben sie jedoch dennoch 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 diejenigen, die von höher priorisierten Operatoren produziert werden. Zum Beispiel muss die rechte Seite des Memberzugriffs
.
(Priorität 17) ein Bezeichner anstelle eines gruppierten Ausdrucks sein. Die linke Seite von Arrow=>
(Priorität 2) muss eine Argumentenliste oder ein einzelner Bezeichner sein, anstelle eines zufälligen Ausdrucks. - Einige Operatoren haben bestimmte Operanden, die Ausdrücke akzeptieren, die breiter sind als diejenigen, die von höher priorisierten Operatoren produziert werden. Zum Beispiel kann der eingeklammert-Ausdruck der Klammernotation
[ … ]
(Priorität 17) jeder Ausdruck sein, sogar durch Komma (Priorität 1) verbundene Ausdrücke. Diese Operatoren verhalten sich, als ob jener Operand "automatisch gruppiert" sei. In diesem Fall werden wir die Assoziativität weglassen.
Priorität | Assoziativität | Einzelne Operatoren | Anmerkungen |
---|---|---|---|
18: Gruppierung | k.A. | Gruppierung(x) |
[1] |
17: Zugriff und Aufruf | von links nach rechts | Memberzugriffx.y |
[2] |
Optionales Chainingx?.y |
|||
k.A. | Berechneter Memberzugriffx[y] |
[3] | |
new mit Argumentenlistenew x(y) |
[4] | ||
Funktionsaufrufx(y) |
|||
import(x) |
|||
16: new | k.A. | new ohne Argumentenlistenew x |
|
15: Postfix-Operatoren | k.A. | Postfix-Inkrementx++ |
[5] |
Postfix-Dekrementx-- |
|||
14: Präfix-Operatoren | k.A. | Präfix-Inkrement++x |
[6] |
Präfix-Dekrement--x |
|||
Logisches NICHT!x |
|||
Bitweises NICHT~x |
|||
Unärer Plus+x |
|||
Unäre Negation-x |
|||
typeof x |
|||
void x |
|||
delete x |
[7] | ||
await x |
|||
13: Potenzierung | von rechts nach links | Potenzierungx ** y |
[8] |
12: Multiplikationsoperatoren | von links nach rechts | Multiplikationx * y |
|
Divisionx / y |
|||
Restx % y |
|||
11: Additionsoperatoren | 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 |
|||
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, Nullish Coalescing | 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 |
|||
Potenzierungszuweisungx **= 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 |
|||
Logisches UND-Zuweisungx &&= y |
|||
Logisches ODER-Zuweisungx ||= y |
|||
Nullish Coalescing Zuweisungx ??= y |
|||
von rechts nach links | Bedingter (ternärer) Operatorx ? y : z |
[11] | |
von rechts nach links | Arrowx => y |
[12] | |
k.A. | yield x |
||
yield* x |
|||
Spread...x |
[13] | ||
1: Komma | von 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 kommagetrennte Liste aus jedem Ausdruck mit Priorität > 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 Eigenschaftszugriff). Seine Priorität bedeutet, dass
new Foo++
als(new Foo)++
(ein Syntaxfehler) und nicht alsnew (Foo++)
(ein TypeError: (Foo++) ist kein Konstruktor) geparst wird. - Der Operand muss ein gültiges Zuweisungsziel sein (Bezeichner oder Eigenschaftszugriff).
- Der Operand kann kein Bezeichner oder Zugriff auf eine private Eigenschaft sein.
- Die linke Seite kann keine Priorität 14 haben.
- Die Operanden dürfen kein logisches ODER
||
oder logisches UND&&
Operator ohne Gruppierung sein. - Die "linke Seite" muss ein gültiges Zuweisungsziel sein (Bezeichner oder Eigenschaftszugriff).
- Die Assoziativität bedeutet, dass die beiden Ausdrücke nach
?
implizit gruppiert sind. - Die "linke Seite" ist ein einziger Bezeichner oder eine geklammerte Parameterliste.
- Nur gültig innerhalb von Objektliteralen, Array-Literalen oder Argumentenlisten.
Die Priorität der Gruppen 17 und 16 kann etwas mehrdeutig sein. Hier sind einige Beispiele zur Klärung.
- Optionales Chaining ist immer durch seine jeweilige Syntax ohne Optionalität substituierbar (bis auf einige spezielle Fälle, in denen Optionales Chaining verboten ist). Zum Beispiel jede Stelle, die
a?.b
akzeptiert, akzeptiert aucha.b
und umgekehrt, und ebenso füra?.()
,a()
, etc. - Memberausdrücke und berechnete Memberausdrücke sind immer gegeneinander austauschbar.
- Aufrufausdrücke und
import()
Ausdrücke sind immer gegeneinander austauschbar. - Dies hinterlä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 (