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 ausconsole.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.
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:
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.
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.
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:
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:
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.
let a = 1;
a++++; // SyntaxError: Invalid left-hand side in postfix operation.
Die Operatorpräzedenz wird rekursiv behandelt. Betrachten Sie zum Beispiel diesen Ausdruck:
Zuerst gruppieren wir Operatoren mit unterschiedlicher Präzedenz nach abnehmenden Präzedenzebenen.
**
-Operator hat die höchste Präzedenz, also wird er zuerst gruppiert.**
-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.*
//
-Ausdruck herum wird +
zuerst gruppiert, da es eine höhere Präzedenz als >>
hat. (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 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.
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.
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 (?.
).
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:
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:
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.
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:
...
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..
(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.[ ⦠]
(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.Anmerkungen:
new
-Ausdrucks kann keine optionale Verkettung sein.new Foo++
ist (new Foo)++
(ein Syntaxfehler) und nicht new (Foo++)
(ein TypeError: (Foo++) ist kein Konstruktor).||
oder logisches UND &&
Operator ohne Gruppierung sein.?
implizit gruppiert sind.Die Präzedenz der Gruppen 17 und 16 kann etwas unklar sein. Hier sind ein paar Beispiele zur Klärung:
a?.b
akzeptiert wird, auch a.b
akzeptiert werden und umgekehrt, und ähnlich für a?.()
, a()
, usw.import()
-Ausdrücke sind immer gegeneinander austauschbar.new
mit Argumenten, Funktionsaufruf und new
ohne Argumente.
a.b.c
), new
mit Argumenten (new a().b
) und Funktionsaufruf (a().b
).new
mit Argumenten kann sein: ein Memberzugriff (new a.b()
) und new
mit Argumenten (new new a()()
).a.b()
), new
mit Argumenten (new a()()
) und Funktionsaufruf (a()()
).new
ohne Argumente kann sein: ein Memberzugriff (new a.b
), new
mit Argumenten (new new a()
), und new
ohne Argumente (new new a
).RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4