Dieser Inhalt wurde automatisch aus dem Englischen übersetzt, und kann Fehler enthalten. Erfahre mehr über dieses Experiment.

View in English Always switch to English

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.

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

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:

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

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

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

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

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

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

Die Operatorpräzedenz wird rekursiv behandelt. Beispielsweise betrachten Sie diesen Ausdruck:

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

Zuerst gruppieren wir Operatoren mit unterschiedlicher Präzedenz nach absteigenden Präzedenzstufen.

  1. Der **-Operator hat die höchste Präzedenz, daher wird er zuerst gruppiert.
  2. 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.
  3. In der Nähe des in 2 gruppierten *//-Ausdrucks wird, da + eine höhere Präzedenz als >> hat, ersteres gruppiert.
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 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.

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

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`

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:

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

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

  1. 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.
  2. 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.
  3. 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 access
x.y | rowspan="2" [2] | | | | Optional chaining
x?.y | | | rowspan="4" n/a | Computed member access
x[y] | [3] | | | | new mit Argumentliste
new x(y) | rowspan="3" [4] | | | | Funktionsaufruf
x(y) | | | | import(x) | | 16: new | n/a | new ohne Argumentliste
new x | | rowspan="2" 15: postfix) Operatoren | rowspan="2" n/a | Postfix increment
x++ | rowspan="2" [5] | | | | Postfix decrement
x-- | | 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 | Exponentiation
x ** y | [8] | | rowspan="3" 12: Multiplikationsoperatoren | rowspan="3" left-to-right | Multiplication
x * y | | | | Division
x / y | | | | Remainder
x % y | | rowspan="2" 11: Additionsoperatoren | rowspan="2" left-to-right | Addition
x + y | | | | Subtraction
x - y | | rowspan="3" 10: Bitweise Verschiebung | rowspan="3" left-to-right | Left shift
x << y | | | | Right shift
x >> y | | | | Unsigned right shift
x >>> y | | rowspan="6" 9: Relationale Operatoren | rowspan="6" left-to-right | Less than
x < y | | | | Less than or equal
x <= y | | | | Greater than
x > y | | | | Greater than or equal
x >= y | | | x in y | | | x instanceof y | | rowspan="4" 8: Gleichheitsoperatoren | rowspan="4" left-to-right | Equality
x == y | | | | Inequality
x != y | | | | Strict equality
x === y | | | | Strict inequality
x !== y | | 7: Bitweise UND | left-to-right | Bitwise AND
x & y | | 6: Bitweise XOR | left-to-right | Bitwise XOR
x ^ y | | 5: Bitweise ODER | left-to-right | Bitwise OR
x | y | | 4: Logisches UND | left-to-right | Logical AND
x && y | | rowspan="2" 3: Logisches ODER, Nullish-Koaleszenz | rowspan="2" left-to-right | Logical OR
x | | y | | | | Nullish coalescing operator
x ?? y | [9] | | rowspan="21" 2: Zuweisung und Verschiedenes | rowspan="16" right-to-left | Assignment
x = y | rowspan="16" [10] | | | | Addition assignment
x += y | | | | Subtraction assignment
x -= y | | | | Exponentiation assignment
x **= y | | | | Multiplication assignment
x *= y | | | | Division assignment
x /= y | | | | Remainder assignment
x %= y | | | | Left shift assignment
x <<= y | | | | Right shift assignment
x >>= y | | | | Unsigned right shift assignment
x >>>= y | | | | Bitwise AND assignment
x &= y | | | | Bitwise XOR assignment
x ^= y | | | | Bitwise OR assignment
x | = y | | | | Logical AND assignment
x &&= y | | | | Logical OR assignment
x | | = y | | | | Nullish coalescing assignment
x ??= y | | right-to-left | Bedingter (ternärer) Operator
x ? y : z | [11] | | right-to-left | Pfeil
x => y | [12] | | rowspan="3" n/a | yield x | | | yield* x | | | Spread
...x | [13] | | 1: Komma | left-to-right | Comma operator
x, y |

Anmerkungen:

  1. Der Operand kann jeder Ausdruck sein.
  2. Die "rechte Seite" muss ein Bezeichner sein.
  3. Die "rechte Seite" kann jeder Ausdruck sein.
  4. 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.
  5. Der Operand muss ein gültiges Zuweisungsziel (Bezeichner oder Eigenschaftszugriff) sein. Seine Präzedenz bedeutet, dass new Foo++ new (Foo++) (ein Syntaxfehler) und nicht new (Foo++) (ein TypeError: (Foo++) ist kein Konstruktor) ist.
  6. Der Operand muss ein gültiges Zuweisungsziel (Bezeichner oder Eigenschaftszugriff) sein.
  7. Der Operand kann kein Bezeichner oder ein privater Zugriff sein.
  8. Die linke Seite kann keine Präzedenz von 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 Zuweisungsziel (Bezeichner oder Eigenschaftszugriff) sein.
  11. Die Assoziativität bedeutet, dass die beiden Ausdrücke nach ? implizit gruppiert sind.
  12. Die "linke Seite" ist ein einzelner Bezeichner oder eine eingeklammertes Parameterliste.
  13. 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, auch a.b und umgekehrt, und ähnliches für a?.(), 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 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).