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.

js
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:

js
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.

js
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.

js
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:

js
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:

js
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.

js
let a = 1;
a++++; // SyntaxError: Invalid left-hand side in postfix operation.

Die Operatorpräzedenz wird rekursiv behandelt. Betrachten Sie zum Beispiel diesen Ausdruck:

js
1 + 2 ** 3 * 4 / 5 >> 6

Zuerst gruppieren wir Operatoren mit unterschiedlicher Präzedenz nach abnehmenden Präzedenzebenen.

  1. Der **-Operator hat die höchste Präzedenz, also wird er zuerst gruppiert.
  2. 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.
  3. Um den in 2 gruppierten *//-Ausdruck herum wird + zuerst gruppiert, da es eine höhere Präzedenz als >> hat.
js
   (1 + ( (2 ** 3) * 4 / 5) ) >> 6
// │    │ └─ 1. ─┘        │ │
// │    └────── 2. ───────┘ │
// └────────── 3. ──────────┘

Innerhalb der *//-Gruppe, da sie beide linksassoziativ sind, würde der linke Operand gruppiert.

js
   (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.

js
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 (?.).

js
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:

js
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:

js
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:

  1. 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.
  2. 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.
  3. 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 access
x.y
[2]
Optional chaining
x?.y
n/a Computed member access
x[y]
[3]
new mit Argumentliste
new x(y)
[4]
Funktionsaufruf
x(y)
import(x)
16: new n/a new ohne Argumentliste
new x
15: Postfix-Operatoren n/a Postfix-inkrement
x++
[5]
Postfix-dekrement
x--
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 Exponentiation
x ** y
[8]
12: Multiplikative Operatoren von links nach rechts Multiplikation
x * y
Division
x / y
Rest
x % y
11: Additive Operatoren von links nach rechts Addition
x + y
Subtraktion
x - y
10: Bitweiser Shift von links nach rechts Linksshift
x << y
Rechtsshift
x >> y
Unsigned Rechtsshift
x >>> y
9: Relationale Operatoren von links nach rechts Kleiner als
x < y
Kleiner oder gleich
x <= y
Größer als
x > y
Größer oder gleich
x >= y
x in y
x instanceof y
8: Gleichheitsoperatoren von links nach rechts Gleichheit
x == y
Ungleichheit
x != y
Strikte Gleichheit
x === y
Strikte Ungleichheit
x !== y
7: Bitweises UND von links nach rechts Bitweises UND
x & y
6: Bitweises XOR von links nach rechts Bitweises XOR
x ^ y
5: Bitweises ODER von links nach rechts Bitweises ODER
x | y
4: Logisches UND von links nach rechts Logisches UND
x && y
3: Logisches ODER, Null-Koaleszenz von links nach rechts Logisches ODER
x || y
Null-Koaleszenzoperator
x ?? y
[9]
2: Zuweisung und Verschiedenes von rechts nach links Zuweisung
x = y
[10]
Addition-Zuweisung
x += y
Subtraktion-Zuweisung
x -= y
Exponentiation-Zuweisung
x **= y
Multiplikation-Zuweisung
x *= y
Division-Zuweisung
x /= y
Rest-Zuweisung
x %= y
Links-Shift-Zuweisung
x <<= y
Rechts-Shift-Zuweisung
x >>= y
Unsigned Rechts-Shift-Zuweisung
x >>>= y
Bitweises UND-Zuweisung
x &= y
Bitweises XOR-Zuweisung
x ^= y
Bitweises ODER-Zuweisung
x |= y
Logisches UND-Zuweisung
x &&= y
Logisches ODER-Zuweisung
x ||= y
Null-Koaleszenz-Zuweisung
x ??= y
von rechts nach links Bedingter (ternärer) Operator
x ? y : z
[11]
von rechts nach links Arrow
x => y
[12]
n/a yield x
yield* x
Spread
...x
[13]
1: Komma von links nach rechts Komma-Operator
x, y

Anmerkungen:

  1. Der Operand kann ein beliebiger Ausdruck sein.
  2. Die "rechte Seite" muss ein Bezeichner sein.
  3. Die "rechte Seite" kann ein beliebiger Ausdruck sein.
  4. 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.
  5. 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 nicht new (Foo++) (ein TypeError: (Foo++) ist kein Konstruktor).
  6. Der Operand muss ein gültiges Ziel einer Zuweisung sein (Bezeichner oder Zugriff auf eine Eigenschaft).
  7. Der Operand kann kein Bezeichner oder ein Zugriff auf ein privates Element sein.
  8. Die linke Seite kann keine Präzedenz 14 haben.
  9. Die Operanden können kein logisches ODER || oder logisches UND && Operator ohne Gruppierung sein.
  10. Die "linke Seite" muss ein gültiges Ziel einer Zuweisung sein (Bezeichner oder Zugriff auf eine Eigenschaft).
  11. Die Assoziativität bedeutet, dass die beiden Ausdrücke nach ? implizit gruppiert sind.
  12. Die "linke Seite" ist ein einzelner Bezeichner oder eine geklammerte Parameterliste.
  13. 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, auch a.b akzeptiert werden und umgekehrt, und ähnlich für a?.(), 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 und new 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()) und new 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()), und new ohne Argumente (new new a).