Operatorpräzedenz
Operatorpräzedenz bestimmt, wie Operatoren im Verhältnis 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 untenstehende Darstellung beschreibbar ist, bei der sowohl OP1
als auch OP2
Platzhalter für Operatoren sind.
a OP1 b OP2 c
Die obenstehende Kombination hat zwei mögliche Interpretationen:
(a OP1 b) OP2 c a OP1 (b OP2 c)
Welche von ihnen die Sprache zu übernehmen entscheidet, 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 als erster vor, und Assoziativität spielt keine Rolle. Beachten Sie, wie Multiplikation eine höhere Präzedenz hat als Addition und zuerst ausgeführt wird, obwohl 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
Innerhalb von Operatoren mit derselben Präzedenz gruppiert die Sprache diese 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, 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 zugewiesenen Wert zurückgibt. Zuerst wird b
auf 5 gesetzt. Dann wird a
ebenfalls auf 5 gesetzt — der Rückgabewert von b = 5
, alias rechter Operand der Zuweisung.
Ein weiteres Beispiel ist der einzigartige Exponentialoperator, 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 und dann, für benachbarte Operatoren mit derselben Präzedenz, nach Assoziativität gruppiert. Werden Division und Exponentiation gemischt, kommt die Exponentiation immer vor der Division. Beispielsweise ergibt 2 ** 3 / 3 ** 2
0,8888888888888888, weil es dasselbe ist wie (2 ** 3) / (3 ** 2)
.
Für präfixe unäre Operatoren nehmen wir folgendes Muster an:
OP1 a OP2 b
wobei OP1
ein präfixer unärer Operator und OP2
ein binärer Operator ist. Hat OP1
eine höhere Präzedenz als OP2
, wird es als (OP1 a) OP2 b
gruppiert; andernfalls als 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äre Operator auf dem zweiten Operanden:
a OP2 OP1 b
Dann muss der binäre Operator OP2
eine niedrigere Präzedenz haben als der unäre Operator OP1
, damit es als a OP2 (OP1 b)
gruppiert wird. Beispielsweise ist Folgendes ungültig:
function* foo() {
a + yield 1;
}
Da +
eine höhere Präzedenz hat als yield
, 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 Stolperstein.
Haben wir zwei präfixe unäre Operatoren:
OP1 OP2 a
Dann muss der unäre Operator, der näher am Operanden liegt, OP2
, eine höhere Präzedenz haben als OP1
, damit es als OP1 (OP2 a)
gruppiert wird. Es ist möglich, es anders herum zu bekommen und bei (OP1 OP2) a
zu landen:
async function* foo() {
await yield 1;
}
Da await
eine höhere Präzedenz als yield
hat, würde dies zu (await yield) 1
, was bedeutet, auf einen Bezeichner namens yield
zu warten, was ein Syntaxfehler ist. Ähnlich, wenn Sie new !A;
haben, da !
eine niedrigere Präzedenz hat als new
, würde dies zu (new !) A
, was offensichtlich ungültig ist. (Dieser Code scheint ohnehin unsinnig zu sein, da !A
immer einen booleschen Wert liefert, keine Konstrukturfunktion.)
Für postfixe unäre Operatoren (nämlich ++
und --
) gelten dieselben Regeln. Glücklicherweise haben beide Operatoren eine höhere Präzedenz als jeder binäre Operator, so dass die Gruppierung immer so ist, wie Sie es erwarten würden. Da ++
zudem einen Wert und keinen Verweis ergibt, können Sie auch keine mehrfachen Inkremente zusammenketten, wie Sie es in C tun könnten.
let a = 1;
a++++; // SyntaxError: Invalid left-hand side in postfix operation.
Die Operatorpräzedenz wird rekursiv behandelt. Beispielsweise betrachten Sie diesen Ausdruck:
1 + 2 ** 3 * 4 / 5 >> 6
Zuerst gruppieren wir Operatoren mit unterschiedlicher Präzedenz nach absteigenden Präzedenzstufen.
- Der
**
-Operator hat die höchste Präzedenz, daher wird er zuerst gruppiert. - In der Nähe des
**
-Ausdrucks hat es*
rechts und+
links.*
hat eine höhere Präzedenz, daher wird es zuerst gruppiert.*
und/
haben die gleiche Präzedenz, also gruppieren wir sie vorerst zusammen. - In der Nähe des in 2 gruppierten
*
//
-Ausdrucks wird, da+
eine höhere Präzedenz als>>
hat, ersteres gruppiert.
(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 Operatorpräzedenz und Assoziativität nur die Reihenfolge der Bewertung von Operatoren (die implizite Gruppierung) beeinflussen, nicht aber die Reihenfolge der Bewertung 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 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 wie an eine post-order Traversierung.
/ ┌────────┴────────┐ echo("left", 4) ** ┌────────┴────────┐ echo("middle", 3) echo("right", 2)
Nachdem alle Operatoren ordnungsgemäß gruppiert wurden, würden die binären Operatoren einen binären Baum bilden. Die Auswertung beginnt mit 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 Operatoren mit höherer Präzedenz 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 Blattknoten — die echo()
-Aufrufe — von links nach rechts besucht werden, unabhängig von der Präzedenz der sie verbindenden Operatoren.
Short-Circuiting
Im vorherigen Abschnitt sagten wir "Die Ausdrücke mit höherer Präzedenz werden immer zuerst ausgewertet" — dies ist im Allgemeinen wahr, muss jedoch mit dem Hinweis auf Short-Circuiting ergänzt werden, bei dem ein Operand möglicherweise überhaupt nicht ausgewertet wird.
Short-Circuiting ist ein Fachbegriff für bedingte Auswertung. Beispielsweise wird im Ausdruck a && (b + c)
, wenn a
falsy ist, der Unterausdruck (b + c)
nicht einmal ausgewertet, auch wenn er gruppiert ist und somit eine höhere Präzedenz als &&
hat. Wir könnten sagen, dass der logische UND-Operator (&&
) "verkürzt" ist. Neben dem logischen UND gehören zu den verkürzten Operatoren auch logisches ODER (||
), Nullish-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`
Beim Auswerten eines verkürzten 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 von Short-Circuiting ist in diese Operatoren eingebaut. Bei anderen Operatoren würden immer beide Operanden ausgewertet, 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 einer post-order Traversierung bleibt bestehen. Nachdem jedoch der linke Teilbaum eines verkürzten Operators besucht wurde, entscheidet die Sprache, ob der rechte Operand ausgewertet werden muss. Wenn 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. Dies bedeutet nicht, dass ||
in diesem Fall eine höhere Präzedenz hat — genau weil (B() && A())
eine höhere Präzedenz hat, wird es als Ganzes vernachlässigt. Wenn es umgestellt wird als:
console.log(A() && B() || C());
// Logs:
// called A
// called C
// true
Dann würde der Short-Circuiting-Effekt von &&
nur verhindern, dass B()
ausgewertet wird, aber da A() && B()
als Ganzes false
ist, würde C()
dennoch ausgewertet.
Beachten Sie jedoch, dass Short-Circuiting das endgültige Bewertungsergebnis nicht ändert. Es beeinflusst nur die Bewertung der Operanden, nicht, wie Operatoren gruppiert werden — wenn die Bewertung der Operanden keine Seiteneffekte hat (zum Beispiel Konsolenausgaben, Zuweisungen zu Variablen, das Werfen eines Fehlers), wäre Short-Circuiting überhaupt nicht beobachtbar.
Auch die Zuweisungsvariante dieser Operatoren (&&=
, ||=
, ??=
) sind verkürzt. Sie sind so verkürzt, dass die Zuweisung überhaupt nicht erfolgt.
Tabelle
Die folgende Tabelle listet die Operatoren in der Reihenfolge von der höchsten Präzedenz (18) bis zur niedrigsten 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 angesehen. Wir haben sie jedoch dennoch aufgenommen, um zu zeigen, wie fest sie im Vergleich zu anderen Operatoren/Ausdrücken binden. - Einige Operatoren haben bestimmte Operanden, die Ausdrücke erfordern, die enger sind als die, die von Operatoren mit höherer Präzedenz produziert werden. Zum Beispiel muss die rechte Seite des Memberzugriffs
.
(Präzedenz 17) ein Bezeichner anstelle eines gruppierten Ausdrucks sein. Die linke Seite des Pfeils=>
(Präzedenz 2) muss eine Argumentliste oder ein einzelner Bezeichner anstelle eines zufälligen Ausdrucks sein. - Einige Operatoren haben bestimmte Operanden, die Ausdrücke akzeptieren, die weiter sind als die, die von Operatoren mit höherer Präzedenz produziert werden. Zum Beispiel kann der klammernverschlossene Ausdruck der Klammernotation
[ … ]
(Präzedenz 17) jeder Ausdruck sein, selbst durch Komma (Präzedenz 1) verbundene. Diese Operatoren wirken so, als wäre dieser Operand "automatisch gruppiert". In diesem Fall werden wir die Assoziativität weglassen.
| Präzedenz | Assoziativität | Einzelne Operatoren | Anmerkungen |
| ------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- | ----------------- | ---------- |
| 18: Gruppierung | n/a | Grouping(x)
| [1] |
| rowspan="6" 17: Zugriff und Aufruf | rowspan="2" left-to-right | Member accessx.y
| rowspan="2" [2] |
| | | Optional chainingx?.y
|
| | rowspan="4" n/a | Computed member accessx[y]
| [3] |
| | | new
mit Argumentlistenew x(y)
| rowspan="3" [4] |
| | | Funktionsaufrufx(y)
|
| | | import(x)
|
| 16: new | n/a | new
ohne Argumentlistenew x
|
| rowspan="2" 15: postfix) Operatoren | rowspan="2" n/a | Postfix incrementx++
| rowspan="2" [5] |
| | | Postfix decrementx--
|
| rowspan="10" 14: Präfix) Operatoren | rowspan="10" n/a | Prefix increment++x
| rowspan="2" [6] |
| | | Prefix decrement--x
|
| | | Logical NOT!x
|
| | | Bitwise NOT~x
|
| | | Unary plus+x
|
| | | Unary negation-x
|
| | | typeof x
|
| | | void x
|
| | | delete x
| [7] |
| | | await x
|
| 13: Exponential | right-to-left | Exponentiationx ** y
| [8] |
| rowspan="3" 12: Multiplikationsoperatoren | rowspan="3" left-to-right | Multiplicationx * y
|
| | | Divisionx / y
|
| | | Remainderx % y
|
| rowspan="2" 11: Additionsoperatoren | rowspan="2" left-to-right | Additionx + y
|
| | | Subtractionx - y
|
| rowspan="3" 10: Bitweise Verschiebung | rowspan="3" left-to-right | Left shiftx << y
|
| | | Right shiftx >> y
|
| | | Unsigned right shiftx >>> y
|
| rowspan="6" 9: Relationale Operatoren | rowspan="6" left-to-right | Less thanx < y
|
| | | Less than or equalx <= y
|
| | | Greater thanx > y
|
| | | Greater than or equalx >= y
|
| | x in y
|
| | x instanceof y
|
| rowspan="4" 8: Gleichheitsoperatoren | rowspan="4" left-to-right | Equalityx == y
|
| | | Inequalityx != y
|
| | | Strict equalityx === y
|
| | | Strict inequalityx !== y
|
| 7: Bitweise UND | left-to-right | Bitwise ANDx & y
|
| 6: Bitweise XOR | left-to-right | Bitwise XORx ^ y
|
| 5: Bitweise ODER | left-to-right | Bitwise ORx | y
|
| 4: Logisches UND | left-to-right | Logical ANDx && y
|
| rowspan="2" 3: Logisches ODER, Nullish-Koaleszenz | rowspan="2" left-to-right | Logical ORx | | y
|
| | | Nullish coalescing operatorx ?? y
| [9] |
| rowspan="21" 2: Zuweisung und Verschiedenes | rowspan="16" right-to-left | Assignmentx = y
| rowspan="16" [10] |
| | | Addition assignmentx += y
|
| | | Subtraction assignmentx -= y
|
| | | Exponentiation assignmentx **= y
|
| | | Multiplication assignmentx *= y
|
| | | Division assignmentx /= y
|
| | | Remainder assignmentx %= y
|
| | | Left shift assignmentx <<= y
|
| | | Right shift assignmentx >>= y
|
| | | Unsigned right shift assignmentx >>>= y
|
| | | Bitwise AND assignmentx &= y
|
| | | Bitwise XOR assignmentx ^= y
|
| | | Bitwise OR assignmentx | = y
|
| | | Logical AND assignmentx &&= y
|
| | | Logical OR assignmentx | | = y
|
| | | Nullish coalescing assignmentx ??= y
|
| right-to-left | Bedingter (ternärer) Operatorx ? y : z
| [11] |
| right-to-left | Pfeilx => y
| [12] |
| rowspan="3" n/a | yield x
|
| | yield* x
|
| | Spread...x
| [13] |
| 1: Komma | left-to-right | Comma operatorx, 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 von Ausdrücken mit einer Präzedenz > 1 (d.h. keine Komma-Ausdrücke). Der Konstruktor eines
new
-Ausdrucks kann keine optionale Kette sein. - Der Operand muss ein gültiges Zuweisungsziel (Bezeichner oder Eigenschaftszugriff) sein. 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 (Bezeichner oder Eigenschaftszugriff) sein.
- Der Operand kann kein Bezeichner oder ein privater Zugriff sein.
- Die linke Seite kann keine Präzedenz von 14 haben.
- Die Operanden können kein logisches ODER
||
oder logisches UND&&
Operator ohne Gruppierung sein. - Die "linke Seite" muss ein gültiges Zuweisungsziel (Bezeichner oder Eigenschaftszugriff) sein.
- Die Assoziativität bedeutet, dass die beiden Ausdrücke nach
?
implizit gruppiert sind. - Die "linke Seite" ist ein einzelner Bezeichner oder eine eingeklammertes Parameterliste.
- Nur gültig in Objektliteralen, Array-Literalen oder Argumentlisten.
Die Präzedenz der Gruppen 17 und 16 kann etwas zweideutig sein. Hier sind einige Beispiele zur Klärung:
- Optionales Chaining ist immer durch seine jeweilige Syntax ohne Optionalität austauschbar (abgesehen von einigen speziellen Fällen, in denen optionales Chaining verboten ist). Zum Beispiel akzeptiert jede Stelle, die
a?.b
akzeptiert, aucha.b
und umgekehrt, und ähnliches füra?.()
,a()
, etc. - Memberausdrucke und berechnete Memberausdrucke sind immer austauschbar.
- Funktionsaufrufe und
import()
-Ausdrucke sind immer 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 (