Приоритет операторов
Приоритет операторов определяет, как операторы парсятся относительно друг друга. Операторы с более высоким приоритетом становятся операндами операторов с более низким приоритетом.
Попробуйте
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
Приоритет и ассоциативность
Рассмотрим выражение, описываемое представлением ниже, где OP1 и OP2 являются заполнителями для операторов.
a OP1 b OP2 c
Приведенная выше комбинация имеет два возможных толкования:
(a OP1 b) OP2 c a OP1 (b OP2 c)
Какое из них выберет язык, зависит от идентичности OP1 и OP2.
Если OP1 и OP2 имеют разные уровни приоритета (см. таблицу ниже), оператор с более высоким приоритетом выполняется первым, а ассоциативность не имеет значения. Обратите внимание, как умножение имеет более высокий приоритет, чем сложение, и выполняется первым, даже если сложение написано первым в коде.
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
В пределах операторов с одинаковым приоритетом язык группирует их по ассоциативности. Левая ассоциативность (слева направо) означает, что выражение интерпретируется как (a OP1 b) OP2 c, тогда как правая ассоциативность (справа налево) означает, что оно интерпретируется как a OP1 (b OP2 c). Операторы присваивания являются правоассоциативными, поэтому вы можете написать:
a = b = 5; // same as writing a = (b = 5);
с ожидаемым результатом, что a и b получают значение 5. Это происходит потому, что оператор присваивания возвращает присваиваемое значение. Сначала b устанавливается в 5. Затем a также устанавливается в 5 — возвращаемое значение b = 5, т.е. правый операнд присваивания.
В качестве другого примера, уникальный оператор возведения в степень имеет правую ассоциативность, тогда как другие арифметические операторы имеют левую ассоциативность.
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...
Операторы сначала группируются по приоритету, а затем, для смежных операторов с одинаковым приоритетом, по ассоциативности. Таким образом, при смешивании деления и возведения в степень, возведение в степень всегда выполняется перед делением. Например, 2 ** 3 / 3 ** 2 дает 0.8888888888888888, потому что это то же самое, что и (2 ** 3) / (3 ** 2).
Для префиксных унарных операторов предположим следующую схему:
OP1 a OP2 b
где OP1 — префиксный унарный оператор, а OP2 — бинарный оператор. Если OP1 имеет более высокий приоритет, чем OP2, то он будет сгруппирован как (OP1 a) OP2 b; в противном случае он будет сгруппирован как OP1 (a OP2 b).
const a = 1; const b = 2; typeof a + b; // Equivalent to (typeof a) + b; result is "number2"
Если унарный оператор находится во втором операнде:
a OP2 OP1 b
Тогда бинарный оператор OP2 должен иметь более низкий приоритет, чем унарный оператор OP1, чтобы он был сгруппирован как a OP2 (OP1 b). Например, следующее недействительно:
function* foo() {
a + yield 1;
}
Поскольку + имеет более высокий приоритет, чем yield, это будет (a + yield) 1 — но поскольку yield является зарезервированным словом в генераторных функциях, это вызвало бы ошибку синтаксиса. К счастью, большинство унарных операторов имеют более высокий приоритет, чем бинарные операторы, и не страдают от этой проблемы.
Если у нас есть два префиксных унарных оператора:
OP1 OP2 a
Тогда унарный оператор, ближайший к операнду, OP2, должен иметь более высокий приоритет, чем OP1, чтобы он был сгруппирован как OP1 (OP2 a). Возможно получить обратный результат и получить (OP1 OP2) a:
async function* foo() {
await yield 1;
}
Поскольку await имеет более высокий приоритет, чем yield, это будет (await yield) 1, что ожидает идентификатор под названием yield, и вызовет ошибку синтаксиса. Аналогично, если у вас есть new !A;, поскольку ! имеет более низкий приоритет, чем new, это будет (new !) A, что, очевидно, неверно. (Этот код выглядит бессмысленным для написания, поскольку !A всегда возвращает булево значение, а не конструктор.)
Для постфиксных унарных операторов (а именно, ++ и --) применяются те же правила. К счастью, оба оператора имеют более высокий приоритет, чем любой бинарный оператор, поэтому группировка всегда соответствует ожиданиям. Более того, поскольку ++ вычисляется как значение, а не как ссылка, вы не можете объединить несколько инкрементов.
let a = 1; a++++; // SyntaxError: Invalid left-hand side in postfix operation.
Приоритет операторов будет обрабатываться рекурсивно. Например, рассмотрим это выражение:
1 + 2 ** 3 * 4 / 5 >> 6
Сначала мы группируем операторы с разным приоритетом по убыванию уровней приоритета.
- Оператор
**имеет самый высокий приоритет, поэтому он группируется первым. - Оглядываясь на выражение
**, оно имеет*справа и+слева.*имеет более высокий приоритет, поэтому он группируется первым.*и/имеют одинаковый приоритет, поэтому мы пока группируем их вместе. - Оглядываясь на выражение
*//, сгруппированное в пункте 2, поскольку+имеет более высокий приоритет, чем>>, группируется первое.
(1 + ( (2 ** 3) * 4 / 5) ) >> 6 // │ │ └─ 1. ─┘ │ │ // │ └────── 2. ───────┘ │ // └────────── 3. ──────────┘
Внутри группы *//, поскольку оба они левоассоциативны, будет сгруппирован левый операнд.
(1 + ( ( (2 ** 3) * 4 ) / 5) ) >> 6 // │ │ │ └─ 1. ─┘ │ │ │ // │ └─│─────── 2. ───│────┘ │ // └──────│───── 3. ─────│──────┘ // └───── 4. ─────┘
Обратите внимание, что приоритет операторов и ассоциативность влияют только на порядок вычисления операторов (неявное группирование), но не на порядок вычисления операндов. Операнды всегда вычисляются слева направо. Выражения с более высоким приоритетом всегда вычисляются первыми, а затем их результаты объединяются в соответствии с порядком приоритета операторов.
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
Если вы знакомы с бинарными деревьями, подумайте об этом как о обходе в постфиксном порядке.
/
┌────────┴────────┐
echo("left", 4) **
┌────────┴────────┐
echo("middle", 3) echo("right", 2)
После того как все операторы будут правильно сгруппированы, бинарные операторы образуют бинарное дерево. Вычисление начинается с внешней группы — это оператор с самым низким приоритетом (в данном случае /). Сначала вычисляется левый операнд этого оператора, который может состоять из операторов с более высоким приоритетом (таких как вызов выражения echo("left", 4)). После вычисления левого операнда правый операнд вычисляется аналогичным образом. Следовательно, все листовые узлы — вызовы echo() — будут посещены слева направо, независимо от приоритета операторов, соединяющих их.
Короткое замыкание
В предыдущем разделе мы сказали, что "выражения с более высоким приоритетом всегда вычисляются первыми" — это, как правило, верно, но это следует дополнить признанием короткого замыкания, в случае которого операнд может вообще не вычисляться.
Короткое замыкание — это жаргон для условного вычисления. Например, в выражении a && (b + c), если a является falsy (ложным), то подвыражение (b + c) даже не будет вычислено, даже если оно сгруппировано и, следовательно, имеет более высокий приоритет, чем &&. Можно сказать, что логический оператор И (&&) "коротко замыкается". Помимо логического И, к другим операторам короткого замыкания относятся логическое ИЛИ (||), нулевое слияние (??) и опциональное цепочка (?.).
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`
При вычислении оператора с коротким замыканием левый операнд всегда вычисляется. Правый операнд будет вычислен только в том случае, если левый операнд не может определить результат операции.
Примечание: Поведение короткого замыкания заложено в эти операторы. Другие операторы всегда вычисляют оба операнда, независимо от того, полезно ли это — например, NaN * foo() всегда вызовет foo, даже если результат никогда не будет чем-то иным, кроме NaN.
Предыдущая модель постфиксного обхода все еще действительна. Однако после посещения левого поддерева оператора с коротким замыканием язык решит, нужно ли вычислять правый операнд. Если нет (например, потому что левый операнд || уже истинный), результат напрямую возвращается без посещения правого поддерева.
Рассмотрим этот случай:
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
Вычисляется только C(), хотя && имеет более высокий приоритет. Это не означает, что || имеет более высокий приоритет в данном случае — именно потому, что (B() && A()) имеет более высокий приоритет, это вызывает полное пренебрежение. Если переставить это как:
console.log(A() && B() || C()); // Logs: // called A // called C // true
Тогда эффект короткого замыкания && предотвратит вычисление только B(), но поскольку A() && B() в целом является false, C() все равно будет вычислен.
Однако обратите внимание, что короткое замыкание не меняет окончательный результат вычисления. Оно влияет только на вычисление операндов, а не на то, как операторы группируются — если вычисление операндов не имеет побочных эффектов (например, логирование в консоль, присваивание переменным, генерация ошибки), короткое замыкание вообще не будет наблюдаться.
Операторы присваивания, соответствующие этим операторам (&&=, ||=, ??=), также имеют короткое замыкание. Они замыкаются таким образом, что присваивание вообще не происходит.
Таблица
Следующая таблица перечисляет операторы в порядке от наивысшего приоритета (18) до самого низкого (1).
Несколько общих замечаний о таблице:
- Не вся синтаксика, включенная сюда, является "операторами" в строгом смысле. Например, spread
...и arrow=>обычно не считаются операторами. Однако мы включили их, чтобы показать, насколько тесно они связываются по сравнению с другими операторами/выражениями. - Некоторые операторы имеют определенные операнды, которые требуют выражений уже, чем те, что производятся операторами с более высоким приоритетом. Например, правая часть доступа к члену
.(приоритет 17) должна быть идентификатором, а не сгруппированным выражением. Левая часть arrow=>(приоритет 2) должна быть списком аргументов или одним идентификатором, а не каким-либо случайным выражением. - Некоторые операторы имеют определенные операнды, которые принимают выражения шире, чем те, что производятся операторами с более высоким приоритетом. Например, выражение в квадратных скобках оператора доступа по индексу
[ … ](приоритет 17) может быть любым выражением, даже объединенным с помощью запятой (приоритет 1). Эти операторы действуют так, как будто этот операнд "автоматически сгруппирован". В этом случае мы опустим ассоциативность.
| Приоритет | Ассоциативность | Отдельные операторы | Примечания |
|---|---|---|---|
| 18: группировка | нет |
Группировка(x)
| [1] |
| 17: доступ и вызов | слева направо |
Доступ к членуx.y
| [2] |
Опциональная цепочкаx?.y
| |||
| нет | Доступ к вычисляемому членуx[y] | [3] | |
new со списком аргументовnew x(y)
| [4] | ||
Вызов функцииx(y) | |||
import(x) | |||
| 16: new | нет |
new без списка аргументовnew x
| |
| 15: постфиксные операторы | нет | Постфиксное увеличениеx++ | [5] |
Постфиксное уменьшениеx-- | |||
| 14: префиксные операторы | нет | Префиксное увеличение++x | [6] |
Префиксное уменьшение--x | |||
Логическое НЕ!x | |||
Побитовое НЕ~x | |||
Унарный плюс+x | |||
Унарное отрицание-x | |||
typeof x | |||
void x | |||
delete x | [7] | ||
await x | |||
| 13: возведение в степень | справа налево | Возведение в степеньx ** y | [8] |
| 12: мультипликативные операторы | слева направо | Умножениеx * y | |
Делениеx / y | |||
Остаток от деленияx % y | |||
| 11: аддитивные операторы | слева направо | Сложениеx + y | |
Вычитаниеx - y | |||
| 10: побитовые сдвиги | слева направо | Сдвиг влевоx << y | |
Сдвиг вправоx >> y | |||
Беззнаковый сдвиг вправоx >>> y | |||
| 9: реляционные операторы | слева направо | Меньшеx < y | |
Меньше или равноx <= y | |||
Большеx > y | |||
Больше или равноx >= y | |||
x in y | |||
x instanceof y | |||
| 8: операторы равенства | слева направо | Равенствоx == y | |
Неравенствоx != y | |||
Строгое равенствоx === y | |||
Строгое неравенствоx !== y | |||
| 7: побитовое И | слева направо | Побитовое Иx & y | |
| 6: побитовое ИЛИ (исключающее) | слева направо | Побитовое ИЛИ (исключающее)x ^ y | |
| 5: побитовое ИЛИ | слева направо | Побитовое ИЛИx | y | |
| 4: логическое И | слева направо | Логическое Иx && y | |
| 3: логическое ИЛИ, оператор нулевого слияния | слева направо | Логическое ИЛИx || y | |
Оператор нулевого слиянияx ?? y | [9] | ||
| 2: присваивание и прочее | справа налево | Присваиваниеx = y | [10] |
Присваивание со сложениемx += y | |||
Присваивание с вычитаниемx -= y | |||
Присваивание с возведением в степеньx **= y | |||
Присваивание с умножениемx *= y | |||
Присваивание с делениемx /= y | |||
Присваивание с остатком от деленияx %= y | |||
Присваивание со сдвигом влевоx <<= y | |||
Присваивание со сдвигом вправоx >>= y | |||
Присваивание с беззнаковым сдвигом вправоx >>>= y | |||
Присваивание с побитовым Иx &= y | |||
Присваивание с побитовым ИЛИ (исключающее)x ^= y | |||
Присваивание с побитовым ИЛИx |= y | |||
Присваивание с логическим Иx &&= y | |||
Присваивание с логическим ИЛИx ||= y | |||
Присваивание с нулевым слияниемx ??= y | |||
| справа налево | Условный (тернарный) операторx ? y : z | [11] | |
| справа налево | Стрелочная функцияx => y | [12] | |
| нет | yield x | ||
yield* x | |||
Spread (расширение)...x | [13] | ||
| 1: оператор запятая | слева направо | Оператор запятаяx, y |
Примечания:
- Операнд может быть любым выражением.
- "Правая часть" должна быть идентификатором.
- "Правая часть" может быть любым выражением.
- "Правая часть" представляет собой разделенный запятыми список любых выражений с приоритетом > 1 (т.е. не являющихся выражениями-запятыми). Конструктор выражения
newне может быть опциональной цепочкой. - Операнд должен быть допустимой целью присваивания (идентификатор или доступ к свойству). Его приоритет означает, что
new Foo++является(new Foo)++(синтаксическая ошибка), а неnew (Foo++)(TypeError: (Foo++) is not a constructor). - Операнд должен быть допустимой целью присваивания (идентификатор или доступ к свойству).
- Операнд не может быть идентификатором или доступом к приватному элементу.
- Левая часть не может иметь приоритет 14.
- Операнды не могут быть оператором логического ИЛИ
||или оператором логического И&&без группировки. - "Левая часть" должна быть допустимой целью присваивания (идентификатор или доступ к свойству).
- Ассоциативность означает, что два выражения после
?неявно сгруппированы. - "Левая часть" — это одиночный идентификатор или список параметров в скобках.
- Допустимо только внутри литералов объектов, литералов массивов или списков аргументов.
Приоритет групп 17 и 16 может быть несколько неоднозначным. Вот несколько примеров для прояснения.
- Опциональная цепочка всегда может быть заменена соответствующим синтаксисом без опциональности (за исключением нескольких особых случаев, когда опциональная цепочка запрещена). Например, любое место, которое принимает
a?.b, также принимаетa.bи наоборот, и аналогично дляa?.(),a()и т.д. - Выражения доступа к членам и вычисляемые выражения доступа к членам всегда взаимозаменяемы.
- Выражения вызова и выражения
import()всегда взаимозаменяемы. - Это оставляет четыре класса выражений: доступ к члену,
newс аргументами, вызов функции иnewбез аргументов.- "Левая часть" выражения доступа к члену может быть: выражением доступа к члену (
a.b.c),newс аргументами (new a().b) и вызовом функции (a().b). - "Левая часть"
newс аргументами может быть: выражением доступа к члену (new a.b()) иnewс аргументами (new new a()()). - "Левая часть" вызова функции может быть: выражением доступа к члену (
a.b()),newс аргументами (new a()()) и вызовом функции (a()()). - Операнд
newбез аргументов может быть: выражением доступа к члену (new a.b),newс аргументами (new new a()) иnewбез аргументов (new new a).
- "Левая часть" выражения доступа к члену может быть: выражением доступа к члену (
© 2005–2025 MDN contributors.
Licensed under the Creative Commons Attribution-ShareAlike License v2.5 or later.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_precedence