Operator precedence
Operator precedence (thứ tự ưu tiên toán tử) xác định cách các toán tử được phân tích cú pháp so với nhau. Các toán tử có độ ưu tiên cao hơn trở thành toán hạng của các toán tử có độ ưu tiên thấp hơn.
Try it
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
Độ ưu tiên và tính kết hợp
Xét một biểu thức có thể mô tả bằng biểu diễn dưới đây, trong đó cả OP1 và OP2 là các chỗ trống cho các toán tử.
a OP1 b OP2 c
Tổ hợp trên có hai cách diễn giải có thể:
(a OP1 b) OP2 c a OP1 (b OP2 c)
Ngôn ngữ chọn cái nào phụ thuộc vào danh tính của OP1 và OP2.
Nếu OP1 và OP2 có mức độ ưu tiên khác nhau (xem bảng bên dưới), toán tử có độ ưu tiên cao hơn sẽ đi trước và tính kết hợp không quan trọng. Hãy quan sát cách nhân có độ ưu tiên cao hơn cộng và được thực thi trước, dù phép cộng được viết trước trong code.
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
Trong các toán tử có cùng độ ưu tiên, ngôn ngữ nhóm chúng theo tính kết hợp. Tính kết hợp trái (trái sang phải) có nghĩa là nó được hiểu là (a OP1 b) OP2 c, trong khi tính kết hợp phải (phải sang trái) có nghĩa là nó được hiểu là a OP1 (b OP2 c). Các toán tử gán có tính kết hợp phải, nên bạn có thể viết:
a = b = 5; // same as writing a = (b = 5);
với kết quả mong đợi là a và b đều nhận giá trị 5. Vì toán tử gán trả về giá trị được gán. Đầu tiên, b được đặt thành 5. Sau đó a cũng được đặt thành 5 — giá trị trả về của b = 5, tức là toán hạng bên phải của phép gán.
Ví dụ khác, toán tử lũy thừa duy nhất có tính kết hợp phải, trong khi các toán tử số học khác có tính kết hợp trái.
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...
Các toán tử được nhóm đầu tiên theo độ ưu tiên, rồi đối với các toán tử kề nhau có cùng độ ưu tiên, theo tính kết hợp. Vì vậy, khi kết hợp phép chia và lũy thừa, lũy thừa luôn đến trước phép chia. Ví dụ, 2 ** 3 / 3 ** 2 cho kết quả 0.8888888888888888 vì nó tương đương với (2 ** 3) / (3 ** 2).
Đối với các toán tử đơn nguyên tiền tố, giả sử chúng ta có mẫu sau:
OP1 a OP2 b
trong đó OP1 là toán tử đơn nguyên tiền tố và OP2 là toán tử nhị phân. Nếu OP1 có độ ưu tiên cao hơn OP2, thì nó sẽ được nhóm thành (OP1 a) OP2 b; ngược lại, nó sẽ là OP1 (a OP2 b).
const a = 1;
const b = 2;
typeof a + b; // Equivalent to (typeof a) + b; result is "number2"
Nếu toán tử đơn nguyên nằm trên toán hạng thứ hai:
a OP2 OP1 b
Thì toán tử nhị phân OP2 phải có độ ưu tiên thấp hơn toán tử đơn nguyên OP1 để được nhóm thành a OP2 (OP1 b). Ví dụ, đoạn sau là không hợp lệ:
function* foo() {
a + yield 1;
}
Vì + có độ ưu tiên cao hơn yield, nó sẽ trở thành (a + yield) 1 — nhưng vì yield là một từ khóa dành riêng trong hàm generator, đây sẽ là lỗi cú pháp. May mắn thay, hầu hết các toán tử đơn nguyên có độ ưu tiên cao hơn các toán tử nhị phân và không gặp phải vấn đề này.
Nếu chúng ta có hai toán tử đơn nguyên tiền tố:
OP1 OP2 a
Thì toán tử đơn nguyên gần toán hạng hơn, OP2, phải có độ ưu tiên cao hơn OP1 để được nhóm thành OP1 (OP2 a). Cũng có thể xảy ra theo chiều ngược lại và kết thúc bằng (OP1 OP2) a:
async function* foo() {
await yield 1;
}
Vì await có độ ưu tiên cao hơn yield, nó sẽ trở thành (await yield) 1, đang chờ một định danh gọi là yield, và đây là lỗi cú pháp. Tương tự, nếu bạn có new !A;, vì ! có độ ưu tiên thấp hơn new, nó sẽ trở thành (new !) A, điều này rõ ràng là không hợp lệ. (Code này trông vô nghĩa để viết, vì !A luôn tạo ra một boolean, không phải hàm constructor.)
Đối với các toán tử đơn nguyên hậu tố (cụ thể là ++ và --), các quy tắc tương tự áp dụng. May mắn thay, cả hai toán tử đều có độ ưu tiên cao hơn bất kỳ toán tử nhị phân nào, vì vậy nhóm luôn đúng như mong đợi. Hơn nữa, vì ++ trả về một giá trị, không phải tham chiếu, bạn không thể nối nhiều phép tăng liên tiếp nhau.
let a = 1;
a++++; // SyntaxError: Invalid left-hand side in postfix operation.
Độ ưu tiên toán tử sẽ được xử lý đệ quy. Ví dụ, xét biểu thức này:
1 + 2 ** 3 * 4 / 5 >> 6
Đầu tiên, chúng ta nhóm các toán tử có độ ưu tiên khác nhau theo các mức độ giảm dần.
- Toán tử
**có độ ưu tiên cao nhất, nên nó được nhóm trước. - Nhìn xung quanh biểu thức
**, nó có*ở bên phải và+ở bên trái.*có độ ưu tiên cao hơn, nên nó được nhóm trước.*và/có cùng độ ưu tiên, nên chúng ta nhóm chúng lại với nhau. - Nhìn xung quanh biểu thức
*//được nhóm ở bước 2, vì+có độ ưu tiên cao hơn>>, cái trước được nhóm.
(1 + ( (2 ** 3) * 4 / 5) ) >> 6
// │ │ └─ 1. ─┘ │ │
// │ └────── 2. ───────┘ │
// └────────── 3. ──────────┘
Trong nhóm *//, vì cả hai đều có tính kết hợp trái, toán hạng bên trái sẽ được nhóm.
(1 + ( ( (2 ** 3) * 4 ) / 5) ) >> 6
// │ │ │ └─ 1. ─┘ │ │ │
// │ └─│─────── 2. ───│────┘ │
// └──────│───── 3. ─────│──────┘
// └───── 4. ─────┘
Lưu ý rằng độ ưu tiên toán tử và tính kết hợp chỉ ảnh hưởng đến thứ tự đánh giá của toán tử (nhóm ngầm định), nhưng không ảnh hưởng đến thứ tự đánh giá của toán hạng. Các toán hạng luôn được đánh giá từ trái sang phải. Các biểu thức có độ ưu tiên cao hơn luôn được đánh giá trước, và kết quả của chúng được tổng hợp theo thứ tự độ ưu tiên toán tử.
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
Nếu bạn quen với cây nhị phân, hãy nghĩ đây là duyệt theo thứ tự sau (post-order traversal).
/
┌────────┴────────┐
echo("left", 4) **
┌────────┴────────┐
echo("middle", 3) echo("right", 2)
Sau khi tất cả các toán tử được nhóm đúng cách, các toán tử nhị phân sẽ tạo thành một cây nhị phân. Đánh giá bắt đầu từ nhóm ngoài cùng — đó là toán tử có độ ưu tiên thấp nhất (/ trong trường hợp này). Toán hạng bên trái của toán tử này được đánh giá trước, có thể được tạo thành từ các toán tử có độ ưu tiên cao hơn (như biểu thức gọi echo("left", 4)). Sau khi toán hạng bên trái được đánh giá, toán hạng bên phải được đánh giá theo cách tương tự. Vì vậy, tất cả các nút lá — các lệnh gọi echo() — sẽ được thăm từ trái sang phải, bất kể độ ưu tiên của các toán tử kết nối chúng.
Short-circuiting
Trong phần trước, chúng ta đã nói "các biểu thức có độ ưu tiên cao hơn luôn được đánh giá trước" — điều này thường đúng, nhưng cần được bổ sung với việc thừa nhận short-circuiting (ngắn mạch đánh giá), trong đó trường hợp một toán hạng có thể hoàn toàn không được đánh giá.
Short-circuiting là thuật ngữ chuyên môn cho đánh giá có điều kiện. Ví dụ, trong biểu thức a && (b + c), nếu a là falsy, thì biểu thức con (b + c) sẽ không được đánh giá, dù nó được nhóm và do đó có độ ưu tiên cao hơn &&. Chúng ta có thể nói rằng toán tử AND logic (&&) được "short-circuit". Cùng với AND logic, các toán tử short-circuit khác bao gồm OR logic (||), nullish coalescing (??), và 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`
Khi đánh giá toán tử short-circuit, toán hạng bên trái luôn được đánh giá. Toán hạng bên phải chỉ được đánh giá nếu toán hạng bên trái không thể xác định kết quả của phép toán.
Note:
Hành vi của short-circuiting được tích hợp sẵn trong các toán tử này. Các toán tử khác sẽ luôn đánh giá cả hai toán hạng, bất kể điều đó có thực sự hữu ích hay không — ví dụ, NaN * foo() sẽ luôn gọi foo, ngay cả khi kết quả sẽ không bao giờ là gì khác ngoài NaN.
Mô hình duyệt theo thứ tự sau trước đó vẫn đứng vững. Tuy nhiên, sau khi cây con bên trái của toán tử short-circuit được thăm, ngôn ngữ sẽ quyết định xem toán hạng bên phải có cần được đánh giá hay không. Nếu không (ví dụ, vì toán hạng bên trái của || đã là truthy), kết quả được trả về trực tiếp mà không cần thăm cây con bên phải.
Xét trường hợp này:
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
Chỉ C() được đánh giá, mặc dù && có độ ưu tiên cao hơn. Điều này không có nghĩa là || có độ ưu tiên cao hơn trong trường hợp này — chính xác là vì (B() && A()) có độ ưu tiên cao hơn khiến nó bị bỏ qua toàn bộ. Nếu sắp xếp lại thành:
console.log(A() && B() || C());
// Logs:
// called A
// called C
// true
Thì hiệu ứng short-circuiting của && chỉ ngăn B() khỏi được đánh giá, nhưng vì A() && B() là false, C() vẫn sẽ được đánh giá.
Tuy nhiên, lưu ý rằng short-circuiting không thay đổi kết quả đánh giá cuối cùng. Nó chỉ ảnh hưởng đến việc đánh giá toán hạng, không phải cách toán tử được nhóm — nếu việc đánh giá các toán hạng không có tác dụng phụ (ví dụ, in ra console, gán cho biến, ném lỗi), short-circuiting sẽ không quan sát được chút nào.
Các biến thể gán của các toán tử này (&&=, ||=, ??=) cũng được short-circuit. Chúng được short-circuit theo cách mà phép gán không xảy ra chút nào.
Bảng
Bảng sau liệt kê các toán tử theo thứ tự từ độ ưu tiên cao nhất (18) đến thấp nhất (1).
Một số ghi chú chung về bảng:
- Không phải tất cả cú pháp được bao gồm ở đây đều là "toán tử" theo nghĩa chặt chẽ. Ví dụ, spread
...và mũi tên=>thường không được coi là toán tử. Tuy nhiên, chúng tôi vẫn đưa vào để cho thấy chúng liên kết chặt chẽ như thế nào so với các toán tử/biểu thức khác. - Một số toán tử có các toán hạng nhất định yêu cầu biểu thức hẹp hơn so với những gì các toán tử có độ ưu tiên cao hơn tạo ra. Ví dụ, vế phải của truy cập thành viên
.(độ ưu tiên 17) phải là định danh thay vì biểu thức được nhóm. Vế trái của mũi tên=>(độ ưu tiên 2) phải là danh sách đối số hoặc một định danh đơn thay vì biểu thức bất kỳ. - Một số toán tử có các toán hạng nhất định chấp nhận biểu thức rộng hơn so với những gì các toán tử có độ ưu tiên cao hơn tạo ra. Ví dụ, biểu thức được đặt trong ngoặc vuông của ký hiệu ngoặc vuông
[ … ](độ ưu tiên 17) có thể là bất kỳ biểu thức nào, kể cả biểu thức nối bằng dấu phẩy (độ ưu tiên 1). Các toán tử này hoạt động như thể toán hạng đó được "tự động nhóm". Trong trường hợp này chúng tôi sẽ bỏ qua tính kết hợp.
| Độ ưu tiên | Tính kết hợp | Toán tử riêng lẻ | Ghi chú |
|---|---|---|---|
| 18: grouping | n/a | Grouping(x) |
[1] |
| 17: access and call | left-to-right | Member accessx.y |
[2] |
Optional chainingx?.y |
|||
| n/a |
Computed member accessx[y]
|
[3] | |
new with argument listnew x(y) |
[4] | ||
Function callx(y)
|
|||
import(x) |
|||
| 16: new | n/a | new without argument listnew x |
|
| 15: postfix operators | n/a |
Postfix incrementx++
|
[5] |
Postfix decrementx--
|
|||
| 14: prefix operators | n/a |
Prefix increment++x
|
[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: exponentiation | right-to-left |
Exponentiationx ** y
|
[8] |
| 12: multiplicative operators | left-to-right |
Multiplicationx * y
|
|
Divisionx / y
|
|||
Remainderx % y
|
|||
| 11: additive operators | left-to-right |
Additionx + y
|
|
Subtractionx - y
|
|||
| 10: bitwise shift | left-to-right |
Left shiftx << y
|
|
Right shiftx >> y
|
|||
Unsigned right shiftx >>> y
|
|||
| 9: relational operators | 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 |
|||
| 8: equality operators | left-to-right |
Equalityx == y
|
|
Inequalityx != y
|
|||
Strict equalityx === y
|
|||
Strict inequalityx !== y
|
|||
| 7: bitwise AND | left-to-right |
Bitwise ANDx & y
|
|
| 6: bitwise XOR | left-to-right |
Bitwise XORx ^ y
|
|
| 5: bitwise OR | left-to-right |
Bitwise ORx | y
|
|
| 4: logical AND | left-to-right |
Logical ANDx && y
|
|
| 3: logical OR, nullish coalescing | left-to-right |
Logical ORx || y
|
|
Nullish coalescing operatorx ?? y
|
[9] | ||
| 2: assignment and miscellaneous | right-to-left |
Assignmentx = y
|
[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 |
Conditional (ternary) operatorx ? y : z
|
[11] | |
| right-to-left |
Arrowx => y
|
[12] | |
| n/a | yield x |
||
yield* x |
|||
Spread...x
|
[13] | ||
| 1: comma | left-to-right |
Comma operatorx, y
|
Ghi chú:
- Toán hạng có thể là bất kỳ biểu thức nào.
- "Vế phải" phải là một định danh.
- "Vế phải" có thể là bất kỳ biểu thức nào.
- "Vế phải" là danh sách được phân tách bằng dấu phẩy của bất kỳ biểu thức nào với độ ưu tiên > 1 (tức là không phải biểu thức dấu phẩy). Constructor của biểu thức
newkhông thể là một optional chain. - Toán hạng phải là đích gán hợp lệ (định danh hoặc truy cập thuộc tính). Độ ưu tiên của nó có nghĩa là
new Foo++là(new Foo)++(lỗi cú pháp) chứ không phảinew (Foo++)(TypeError: (Foo++) không phải là constructor). - Toán hạng phải là đích gán hợp lệ (định danh hoặc truy cập thuộc tính).
- Toán hạng không thể là định danh hay truy cập private element.
- Vế trái không thể có độ ưu tiên 14.
- Các toán hạng không thể là toán tử OR logic
||hoặc AND logic&&mà không được nhóm. - "Vế trái" phải là đích gán hợp lệ (định danh hoặc truy cập thuộc tính).
- Tính kết hợp có nghĩa là hai biểu thức sau
?được nhóm ngầm định. - "Vế trái" là một định danh đơn hoặc danh sách tham số được đặt trong ngoặc đơn.
- Chỉ hợp lệ bên trong object literal, array literal, hoặc danh sách đối số.
Độ ưu tiên của các nhóm 17 và 16 có thể hơi mơ hồ. Dưới đây là một vài ví dụ để làm rõ.
- Optional chaining luôn có thể thay thế cho cú pháp tương ứng không có optionality (ngoại trừ một vài trường hợp đặc biệt mà optional chaining bị cấm). Ví dụ, bất kỳ nơi nào chấp nhận
a?.bcũng chấp nhậna.bvà ngược lại, tương tự choa?.(),a(), v.v. - Member expression và computed member expression luôn có thể thay thế nhau.
- Call expression và
import()expression luôn có thể thay thế nhau. - Điều này để lại bốn lớp biểu thức: truy cập thành viên,
newvới đối số, lời gọi hàm, vànewkhông có đối số.- "Vế trái" của truy cập thành viên có thể là: truy cập thành viên (
a.b.c),newvới đối số (new a().b), và lời gọi hàm (a().b). - "Vế trái" của
newvới đối số có thể là: truy cập thành viên (new a.b()) vànewvới đối số (new new a()()). - "Vế trái" của lời gọi hàm có thể là: truy cập thành viên (
a.b()),newvới đối số (new a()()), và lời gọi hàm (a()()). - Toán hạng của
newkhông có đối số có thể là: truy cập thành viên (new a.b),newvới đối số (new new a()), vànewkhông có đối số (new new a).
- "Vế trái" của truy cập thành viên có thể là: truy cập thành viên (