Перейти к основному содержимому

5.01. Выражения и операторы

Разработчику Архитектору

Выражения и операторы

JavaScript — язык, в котором практически всё есть выражение. Это фундаментальное свойство, определяющее синтаксис и семантику языка: от простейших вычислений до сложных императивных конструкций. Чтобы говорить о поведении кода осознанно, необходимо чётко разделять выражения (expressions) и операторы (operators), понимать их взаимосвязь, приоритеты, ассоциативность и контекст вычисления. Эта глава посвящена систематическому описанию этих понятий — от первичных элементов до сложных композиций, используемых в реальных программах.

Что такое выражение?

Выражение — это любой фрагмент кода, который вычисляется и возвращает значение. В отличие от инструкций (statements), которые описывают действия, выражения существуют ради своего результата. Многие инструкции в JavaScript могут содержать выражения, но не каждое выражение является инструкцией в строгом смысле — хотя благодаря особенностям синтаксиса JavaScript (например, отсутствию строгого разделения expression и statement в контексте блока) эта грань часто размыта.

Примеры выражений:

  • 42 — литерал, выражение, возвращающее число.
  • "привет" — строковый литерал.
  • x + y — бинарное выражение, возвращающее сумму.
  • f() — вызов функции, выражение, возвращающее результат её выполнения.
  • { a: 1 } — литерал объекта.
  • true ? 'да' : 'нет' — условное выражение.
  • a = b = 5 — цепочка присваиваний, возвращающая значение 5.

Любое выражение имеет тип и значение (которое может быть undefined, null, 0, NaN, объектом и так далее), и может быть использовано в любом контексте, где ожидается значение: в правой части присваивания, в качестве аргумента функции, в условии if, в возвращаемом значении и так далее.

Важно: в JavaScript даже блок кода в фигурных скобках { ... } может быть выражением (например, в стрелочной функции с телом-блоком), но только если он находится в подходящем контексте — например, внутри do-выражения (предложение на стадии Stage 3 на момент 2025 года) или как часть switch-выражения в будущих версиях. На текущий момент (ECMAScript 2025) блок сам по себе — инструкция, но не выражение; однако многие синтаксические конструкции, внешне похожие на инструкции, на самом деле являются выражениями.

Что такое оператор?

Оператор — это символ или ключевое слово, которое применяется к одному или нескольким операндам (выражениям) и образует новое выражение. Операторы определяют операции: арифметические, логические, побитовые, сравнения, присваивания и другие.

Классификация операторов по количеству операндов:

  • Унарные — требуют один операнд (например, typeof x, +y, !flag);
  • Бинарные — требуют два операнда (например, a + b, x === y, p && q);
  • Тернарные — единственный в языке: условный оператор ? :, требует три операнда.

Операторы обладают приоритетом (precedence) и ассоциативностью (associativity), определяющими порядок вычисления в составных выражениях. Например, в a + b * c сначала вычисляется b * c, потому что * имеет более высокий приоритет, чем +. Если приоритеты равны (например, a - b - c), ассоциативность (в данном случае левая) определяет, что вычисление идёт слева направо: (a - b) - c.

Рассмотрим категории операторов и выражений в JavaScript последовательно, от простейших к более сложным.


Первичные выражения

Первичные выражения — это атомарные единицы, из которых строятся более сложные конструкции. Они не содержат в себе других операторов и служат отправной точкой для разбора любого выражения.

1. Литералы

Литералы — это непосредственные значения, записанные в коде. Каждый тип данных имеет свой синтаксис литералов:

  • Числовые: 42, 3.14, 0xFF, 0b1010, 1e6;
  • Строковые: 'одинарные', "двойные", `шаблонные ${expr}` (шаблонные строки будут рассмотрены отдельно);
  • Логические: true, false;
  • null и undefined;
  • Регулярные выражения: /abc/gi;
  • BigInt: 123n.

Литералы всегда вычисляются в значения соответствующих типов. Стоит подчеркнуть, что строковые литералы в одинарных и двойных кавычках семантически эквивалентны — различие чисто синтаксическое (удобство экранирования). Шаблонные строки, в отличие от них, являются вызовом конструктора, и их вычисление включает интерполяцию и, при наличии тега, вызов функции-тега.

2. Идентификаторы

Имя переменной, параметра или свойства, определённое в текущей области видимости, — это выражение, возвращающее значение, связанное с этим именем. Например, x, count, isReady. Разрешение имени происходит по цепочке областей видимости (scope chain), и если имя не найдено, возникает ReferenceError (если только не используется в контексте typeof, который допускает неопределённые идентификаторы).

3. Ключевое слово this

this — особое выражение, значение которого определяется контекстом вызова, а не местом определения. В глобальном контексте (вне функций и в не-strict mode) this ссылается на глобальный объект (window в браузере, global в Node.js); в strict mode — undefined. В методах объекта this указывает на объект, через который вызван метод. В стрелочных функциях this наследуется от лексического окружения. Конструкторы и bind/call/apply также влияют на this.

4. Литералы массивов и объектов

  • [1, 2, 3] — массив, выражение, создающее новый объект Array;
  • { x: 1, y: 2 } — объект, создаётся экземпляр Object с указанными свойствами.

Эти конструкции — выражения, а не инструкции. Их можно использовать в любом месте, где ожидается значение: присвоить переменной, передать в функцию, вернуть из неё. Синтаксис допускает вычисляемые ключи ({ [key]: value }), сокращённые методы, геттеры/сеттеры — всё это входит в единый механизм инициализации объектов.

5. Литералы функций

Функции в JavaScript — объекты первого класса. Их можно определять прямо в выражениях:

  • Анонимное функциональное выражение: function (x) { return x * 2; };
  • Именованное функциональное выражение: function double(x) { return x * 2; } (имя double доступно только внутри функции);
  • Стрелочная функция: x => x * 2 или (x, y) => ({ sum: x + y }).

Функциональное выражение создаёт объект функции и возвращает ссылку на него. Оно может быть использовано в любом контексте, где нужен объект: присвоено переменной, передано как аргумент, возвращено из другой функции.

6. Литералы классов

Классы в ES6 — это синтаксический сахар над функциями-конструкторами, но с ключевыми отличиями в семантике (например, строгий режим по умолчанию, отсутствие подъёма). Класс-выражение:

const MyClass = class {
constructor(name) {
this.name = name;
}
greet() {
return `Привет, ${this.name}`;
}
};

Такой литерал — выражение, возвращающее конструктор класса. Он может быть анонимным (class { ... }) или именованным (class MyClass { ... }), причём имя в последнем случае также доступно только внутри тела класса.

7. Шаблонные строки

Шаблонные строки — это выражения, заключённые в обратные кавычки: `Привет, ${name}!`. При вычислении интерполируются значения, возвращаемые вложенными выражениями (${...}), и конкатенируются в итоговую строку.

Если перед шаблонной строкой стоит идентификатор (например, tag), это становится тегированной шаблонной строкой — особой формой вызова функции:

function tag(strings, ...values) {
return strings[0] + values[0].toUpperCase() + strings[1];
}

const name = "мир";
const result = tag`Привет, ${name}!`; // → "Привет, МИР!"

В этом случае tag получает массив «голых» строковых частей и список значений выражений. Это мощный инструмент для создания DSL, безопасного экранирования, локализации и других метапрограммных задач.


Левосторонние выражения (Left-Hand Side Expressions)

Эта категория объединяет выражения, которые могут стоять слева от оператора присваивания или использоваться для вызова (то есть обозначают место в памяти или свойство, доступное для записи или вызова).

1. Доступ к свойствам

  • Точечная нотация: obj.prop — компилятором трактуется как obj["prop"], но ключ должен быть валидным идентификатором;
  • Квадратные скобки: obj[key] — позволяет использовать динамические или некорректные с точки зрения идентификаторов ключи (obj["123"], obj["class"], obj[someVar]).

Оба способа возвращают ссылку на свойство — «место», откуда значение берётся и куда может быть записано. Это критически важно для понимания присваивания и мутаций.

2. Вызовы

Вызов функции f(), метода obj.method(), конструктора new C() — технически это операции над левосторонними выражениями. Выражения f, obj.method, C сначала вычисляются как ссылки, затем к ним применяется операция вызова.

3. Оператор new

new Date()
new MyClass(arg)

Оператор new создаёт новый объект, устанавливает его [[Prototype]] на C.prototype, вызывает функцию C с this, указывающим на новый объект, и возвращает либо результат вызова (если это объект), либо сам объект. Это выражение возвращает ссылку на созданный экземпляр.

4. super

Ключевое слово super используется в методах классов для доступа к свойствам и вызова методов родительского класса. super.method() эквивалентно вызову Object.getPrototypeOf(this).method.call(this), но с корректной привязкой this и проверками контекста (можно использовать только внутри методов класса, не в стрелочных функциях и не в обычных функциях).

5. import.meta

Метаданные текущего модуля: import.meta.url содержит абсолютный URL модуля, import.meta.resolve() (в некоторых средах) позволяет разрешать относительные пути. Это выражение доступно только в модульном контексте и возвращает объект метаданных.


Унарные операторы

Унарные операторы применяются к одному операнду и формируют выражение, возвращающее новое значение (или, в случае некоторых, побочный эффект). Порядок применения: оператор стоит слева от операнда (префиксная форма), за исключением постфиксных ++ и --.

1. typeof

Оператор typeof возвращает строку, указывающую тип операнда на этапе выполнения. Он безопасен: не вызывает ошибок даже при обращении к необъявленным переменным (в отличие от большинства других операций).

Результаты typeof:

ОперандРезультатКомментарий
undefined"undefined"
null"object"Историческая ошибка, сохранённая для обратной совместимости. Это не объект, но результат фиксирован.
true, false"boolean"
Числа (42, 3.14, NaN)"number"NaN — тоже число по типу.
123n"bigint"
Строки"string"
Символы (Symbol())"symbol"
Функции"function"Не отдельный тип, а подмножество объектов, но typeof делает исключение.
Любой другой объект"object"Включая массивы, даты, регулярки, обычные объекты.

Примеры:

typeof undeclaredVariable; // "undefined" — безопасно!
typeof null; // "object"
typeof []; // "object"
typeof /regex/; // "object" (в большинстве реализаций)
typeof (() => {}); // "function"

typeof работает на уровне runtime type. Он не различает массивы и обычные объекты — для этого нужны другие средства (Array.isArray()).

2. void

Оператор void вычисляет свой операнд, отбрасывает результат и возвращает undefined. Чаще всего используется в двух контекстах:

  • Гарантированное получение undefined, независимо от возможного переопределения глобального undefined (актуально в старых средах, где undefined был изменяемым):

    const trulyUndefined = void 0;
  • В javascript:-ссылках, чтобы предотвратить переход или возврат значения:

    <a href="javascript:void(0)">Нажми</a>

Любое выражение после void будет выполнено (включая побочные эффекты), но результат всегда undefined.

3. delete

Оператор delete пытается удалить свойство объекта. Его поведение часто неправильно интерпретируется.

  • delete obj.prop — возвращает true, если свойство успешно удалено или не существовало; false, если свойство неконфигурируемо (например, наследуется от Object.prototype, или объявлено как configurable: false).
  • delete variableне удаляет переменную, возвращает true в нестрогом режиме, SyntaxError в strict mode.
  • delete arr[index] — не удаляет элемент из массива, а делает ячейку «дыркой» (hole), длина массива не меняется, и arr[index] станет undefined (но index in arrfalse).

Пример:

const obj = { a: 1, b: 2 };
Object.defineProperty(obj, 'c', { value: 3, configurable: false });

delete obj.a; // true, 'a' удалён
'a' in obj; // false

delete obj.c; // false, 'c' не удалён
'c' in obj; // true

delete не освобождает память напрямую — он лишь удаляет привязку свойства к объекту. Сборщик мусора решает, освобождать ли память.

4. Унарные + и -

  • Унарный + преобразует операнд в число по алгоритму ToNumber. Эффективная замена Number(x).

    + "42"    // 42
    + true // 1
    + false // 0
    + null // 0
    + undefined // NaN
    + [] // 0
    + [1] // 1
    + [1,2] // NaN
  • Унарный - делает то же самое, но дополнительно меняет знак. Для NaN и Infinity знак меняется соответствующе (-Infinity, -0).

Особое внимание — -0. Это отдельное значение типа number, эквивалентное 0 в сравнениях (-0 === 0true), но различимое через Object.is(-0, 0)false или 1 / -0-Infinity.

5. Логическое отрицание !

Оператор ! преобразует операнд к логическому значению (по ToBoolean) и инвертирует его. Двукратное применение (!!x) — идиома для явного приведения к boolean.

Ложные значения (falsy) в JavaScript:
false, 0, -0, 0n, "", null, undefined, NaN.

Все остальные — истинные (truthy).

!""        // true
!"hello" // false
!!{} // true
!![] // true
!!null // false

6. Побитовое НЕ ~

Применяет поразрядное дополнение: инвертирует все биты 32-битного целого представления операнда.

Поскольку ~x эквивалентно -(x + 1), часто используется в идиомах вроде:

if (~str.indexOf('подстрока')) { ... }

— потому что indexOf возвращает -1, если не найдено, а ~(-1) === 0 (ложь), иначе — ненулевое число (истина). Однако в современном коде это считается плохой практикой; предпочтительнее str.includes('...').

7. Инкремент и декремент: ++, --

Существуют в двух формах:

  • Постфиксная: x++, x-- — возвращает старое значение, затем изменяет переменную.
  • Префиксная: ++x, --x — сначала изменяет переменную, затем возвращает новое значение.

Применяются только к ссылкам (переменным, свойствам, элементам массива), не к выражениям (++(x + y) — ошибка).

let a = 5;
let b = a++; // b = 5, a = 6
let c = ++a; // c = 7, a = 7

Важно: операторы создают побочный эффект — изменяют состояние. Их избыточное использование в сложных выражениях (arr[i++] = ++i) ведёт к нечитаемому и неопределённому поведению и строго не рекомендуется.

8. await

Оператор await может использоваться только внутри async-функции. Он приостанавливает выполнение функции до тех пор, пока промис (или thenable) не завершится, и возвращает:

  • значение — если промис выполнился успешно;
  • бросает исключение — если промис отклонён.

Если операнд не промис — await заворачивает его в Promise.resolve() и немедленно возвращает значение.

async function f() {
const val = await 42; // 42
const res = await fetch('/'); // ждёт ответа
return res.json();
}

await делает асинхронный код похожим на синхронный, но не блокирует поток — выполнение других задач (например, обработчиков событий) продолжается.


Арифметические операторы

Арифметические операторы выполняют математические операции над числовыми значениями. Все (кроме +) преобразуют операнды к числу через ToNumber. Оператор + имеет двойное назначение: сложение чисел и конкатенация строк — и его поведение зависит от типов операндов.

1. Сложение +

Алгоритм:

  1. Вычисляются оба операнда.
  2. Каждый преобразуется к примитиву (ToPrimitive, предпочтительно строка, если один из операндов — строка).
  3. Если хотя бы один операнд — строка, происходит конкатенация.
  4. Иначе — числовое сложение.

Примеры:

1 + 2         // 3
"1" + 2 // "12"
1 + "2" // "12"
"hello" + [] // "hello" (массив → "" → конкатенация)
[] + [] // "" (оба → "", складываются)
{} + [] // "[object Object]" — осторожно: {} интерпретируется как блок в начале выражения!
({} + []) // "[object Object]" — теперь {} — объект.

Особые случаи:

  • +0 + -0+0
  • Infinity + (-Infinity)NaN
  • NaN + что угодноNaN

2. Вычитание -, умножение *, деление /, остаток %, возведение в степень **

Все эти операторы работают строго в числовом контексте: оба операнда приводятся к number (или bigint, но нельзя смешивать типы).

  • -: 5 - 23; 5 - "2"3; "5" - "2"3

  • *: 3 * 412; "3" * null0

  • /: 7 / 23.5; 1 / 0Infinity; -1 / 0-Infinity

  • %: остаток от деления, не математический модуль. Знак результата совпадает со знаком делимого:

    7 % 3   // 1
    -7 % 3 // -1
    7 % -3 // 1
    -7 % -3 // -1

    В отличие от Math.abs(x) % n, % не даёт «положительный остаток» — для этого нужна собственная реализация.

  • **: 2 ** 38; 2 ** -10.5; (-2) ** 0.5NaN (корень из отрицательного — комплексное, не поддерживается).

Особенности чисел в JavaScript

  • Все числа — 64-битные числа с плавающей точкой по стандарту IEEE 754 (тип number), кроме BigInt.
  • Точность ограничена: 0.1 + 0.2 !== 0.3true. Это следствие двоичного представления десятичных дробей.
  • Number.MAX_SAFE_INTEGER (2⁵³ - 1) — максимальное целое, которое можно точно представить. Для больших целых — BigInt.

Операторы сравнения и равенства

Сравнения делятся на строгие и нестрогие, а также на абстрактные и семантические (например, in, instanceof).

1. Абстрактное равенство ==

Оператор == пытается привести операнды к одному типу перед сравнением. Алгоритм сложен и содержит множество специальных правил. Вот основные случаи:

  • null == undefinedtrue (единственная «честная» пара).
  • Число и строка: строка → число, затем числовое сравнение.
  • Булево: true1, false0, затем сравнение.
  • Объект и примитив: объект → примитив (ToPrimitive), затем сравнение.
  • NaN == NaNfalse (всегда).

Примеры-ловушки:

0 == false     // true
"" == false // true
"0" == false // true
[] == false // true ( [] → "" → 0 → false )
[0] == false // true ( [0] → "0" → 0 )
[1] == true // true ( [1] → "1" → 1 )
[1,2] == "1,2" // true

Из-за непредсказуемости и трудностей отладки рекомендуется избегать == в пользу строгого равенства.

2. Строгое равенство ===

Проверяет равенство без приведения типов:

  • Если типы различаются → false.
  • Для чисел: +0 === -0true, NaN === NaNfalse.
  • Для строк, булевых, null, undefined, symbol, bigint — по значению.
  • Для объектов — по ссылке (два объекта равны, только если это один и тот же объект в памяти).

Для сравнения NaN используется Object.is(NaN, NaN)true, а также Object.is(+0, -0)false.

3. Операторы неравенства: !=, !==, <, >, <=, >=

  • != и !== — отрицания == и ===.
  • Операторы <, > и т.д. при сравнении:
    • Строк — лексикографически (по кодам UTF-16);
    • Чисел — численно;
    • Смешанных типов — приведение к числу (кроме случая, когда один из операндов — строка, а другой — объект: сначала объект → примитив, и если получилась строка — лексикография).
"2" > "10"   // true (сравнение кодов: '2' > '1')
2 > "10" // false (2 > 10)
"2" > 10 // false
"12" > "2" // false — лексикография: '1' < '2'

4. Оператор in

Проверяет, существует ли собственное или унаследованное свойство с указанным именем в объекте.

"length" in []        // true (наследуется от Array.prototype)
"push" in [] // true
"toString" in {} // true (от Object.prototype)
"a" in { a: undefined } // true — важно: проверяется *существование*, а не значение
"a" in {} // false

Имя свойства всегда приводится к строке. Для проверки собственных свойств — Object.hasOwn(obj, key) (ранее Object.prototype.hasOwnProperty.call).

5. Оператор instanceof

Проверяет, входит ли прототип правого операнда в цепочку прототипов левого.

[] instanceof Array      // true
[] instanceof Object // true
new Date() instanceof Date // true

Алгоритм: left instanceof right эквивалентен right[Symbol.hasInstance](left), если определён, иначе — проверке left.[[Prototype]] === right.prototype рекурсивно.

Особенности:

  • Не работает между разными контекстами (например, массив из другого фрейма не будет instanceof Array в текущем);
  • Для встроенных типов надёжнее использовать Array.isArray(), typeof и т.д.

Логические и условные операторы

Логические операторы в JavaScript не ограничиваются булевой алгеброй: их поведение расширено до работы с любыми значениями, и они активно используются для управления потоком данных и построения безопасных выражений.

1. Логическое И: &&

Оператор && вычисляет выражения слева направо и возвращает:

  • первое ложное значение (falsy), если оно встречается;
  • последнее истинное значение (truthy), если все истинны.

Это короткозамкнутое вычисление (short-circuit evaluation): правый операнд вычисляется только если левый истинен.

true && "hello"      // "hello"
false && expensiveFn() // false — expensiveFn() не вызывается
null && "world" // null
0 && 1 // 0
"user" && obj.name // obj.name — только если "user" истинно (всегда)

Идиомы:

  • Безопасная проверка цепочки свойств (до появления optional chaining):
    const name = user && user.profile && user.profile.name;
  • Выполнение побочного эффекта при условии:
    isValid && console.log("Валидация пройдена");

Важно: && не приводит результат к boolean. Он возвращает значение одного из операндов, что делает его мощным инструментом для управления потоком данных, но требует осторожности в условиях, где ожидается строго true/false.

2. Логическое ИЛИ: ||

Аналогично, || возвращает:

  • первое истинное значение;
  • последнее ложное, если все ложны.

Короткозамкнутое: правый операнд вычисляется, только если левый ложен.

false || "default"   // "default"
0 || 1 // 1
null || undefined // undefined
"name" || expensiveFn() // "name" — expensiveFn() не вызывается

Идиомы:

  • Задание значений по умолчанию (устаревшая практика для параметров):

    function greet(name) {
    name = name || "Гость"; // опасно: если name = 0 или "", будет "Гость"
    return `Привет, ${name}`;
    }

    → Сегодня предпочтительнее параметры по умолчанию: function greet(name = "Гость").

  • Накопление значений:

    const value = config.option || env.OPTION || DEFAULT;

3. Оператор нулевого слияния: ??

Появился в ES2020 для устранения недостатков ||. Оператор ?? возвращает правый операнд только если левый — null или undefined. В отличие от ||, он игнорирует другие ложные значения (0, "", false).

0 ?? "default"       // 0
"" ?? "default" // ""
false ?? "default" // false
null ?? "default" // "default"
undefined ?? "fallback" // "fallback"

Это позволяет безопасно задавать значения по умолчанию без нарушения семантики нуля или пустой строки.

Комбинирование с && и ||:
Приоритет ?? ниже, чем у && и ||, но выше, чем у =, += и т.д. Однако запрещено писать a || b ?? c без скобок — это вызовет синтаксическую ошибку, чтобы избежать неоднозначности.

Правильно:

(a || b) ?? c
a || (b ?? c)

4. Условный (тернарный) оператор: ? :

Единственный тернарный оператор: условие ? выражение1 : выражение2.

Алгоритм:

  1. Вычисляется условие;
  2. Если ToBoolean(условие)true, вычисляется и возвращается выражение1;
  3. Иначе — выражение2.

Оба выражения не вычисляются заранее — действует короткое замыкание.

const status = isLoading ? "загрузка..." : data ? "готово" : "ошибка";

Ключевое отличие от if…else: тернарный оператор — выражение, а не инструкция. Он возвращает значение и может использоваться везде, где ожидается значение: в присваивании, возврате, аргументах, шаблонных строках.

return user.isAdmin ? adminView : userView;
const message = `Счёт: ${balance >= 0 ? balance : "отрицательный"}`;

Рекомендации:

  • Избегайте вложенных тернарных операторов глубже одного уровня — это ухудшает читаемость.
  • Не используйте для побочных эффектов (например, cond ? f() : g() допустимо, но cond ? x = 1 : y = 2 — лучше через if).
  • При длинных ветках — выносите в переменные или используйте if.

Битовые и побитовые операторы

Битовые операторы работают с 32-битными целыми числами со знаком в дополнительном коде (two’s complement). Перед операцией операнды приводятся к ToInt32, дробная часть отбрасывается, а старшие биты — усекаются.

1. Побитовые логические операторы

ОператорНазваниеОписание
&ИВозвращает 1 в бите, если оба бита = 1
|ИЛИВозвращает 1, если хотя бы один бит = 1
^Исключающее ИЛИВозвращает 1, если биты различны
~НЕ (уже рассмотрен)Инвертирует все биты

Пример:

5 & 3   // 1 — 0b101 & 0b011 = 0b001
5 | 3 // 7 — 0b101 | 0b011 = 0b111
5 ^ 3 // 6 — 0b101 ^ 0b011 = 0b110

Применения:

  • Маскирование флагов: flags & PERMISSION_READ
  • Установка флагов: flags |= PERMISSION_WRITE
  • Инверсия флагов: flags ^= TOGGLE_MODE

2. Побитовые сдвиги

ОператорНазваниеОписание
<<Левый сдвигСдвигает биты влево, заполняя нулями справа. Эквивалентно умножению на 2ⁿ (но с усечением).
>>Правый сдвиг со знакомСохраняет знак: заполняет слева старшим битом (для отрицательных — единицами).
>>>Правый сдвиг без знакаВсегда заполняет нулями слева. Результат — беззнаковое 32-битное целое (всегда ≥ 0).

Примеры:

8 << 1    // 16
-8 >> 1 // -4
-8 >>> 1 // 2147483644 — интерпретируется как большое положительное число
1 << 31 // -2147483648 — переполнение в signed int

Важно: все сдвиги работают с n % 32, то есть x << 33 эквивалентно x << 1.

Когда использовать битовые операции?

  • Работа с низкоуровневыми данными (парсинг бинарных протоколов, шейдеры в WebGL);
  • Оптимизация флагов (часто в играх, эмуляторах);
  • Хэширование, криптографические примитивы.

В повседневной веб-разработке они редки — и их использование требует комментариев и обоснования.


Операторы присваивания

Оператор присваивания (=) — один из самых фундаментальных. Он вычисляет правый операнд, затем записывает результат в левостороннее выражение (LHS), и возвращает присвоенное значение.

1. Простое присваивание: =

a = b = 5; // сначала b = 5 → 5, затем a = 5 → 5

Ассоциативность — правая, поэтому цепочки работают как ожидается.

2. Составные присваивания

Комбинируют операцию и присваивание:

ОператорЭквивалент
+=a = a + b
-=a = a - b
*=a = a * b
/=a = a / b
%=a = a % b
**=a = a ** b
&=a = a & b
|=a = a | b
^=a = a ^ b
<<=a = a << b
>>=a = a >> b
>>>=a = a >>> b

Особенность: левый операнд вычисляется один раз, что важно при работе со свойствами:

obj.prop += 1;
// эквивалентно:
// let temp = obj;
// temp.prop = temp.prop + 1;
// (а не obj.prop = obj.prop + 1 — obj не вычисляется дважды)

Это предотвращает ошибки при изменении obj между обращениями.

3. Деструктурирующее присваивание

Позволяет извлекать данные из массивов и объектов в переменные по шаблону.

Деструктуризация массива:

const [first, second, ...rest] = [10, 20, 30, 40];
// first = 10, second = 20, rest = [30, 40]

const [x, , y] = [1, 2, 3]; // пропуск элемента: x = 1, y = 3

const [a = 1, b = 2] = [undefined, 3]; // a = 1 (по умолчанию), b = 3

Деструктуризация объекта:

const { name, age: years, active = true } = { name: "Тимур", age: 30 };
// name = "Тимур", years = 30, active = true

const { length } = "привет"; // length = 6 — работает с любым итерируемым/объектом

Вложенные шаблоны:

const {
user: { name, role: { title } },
settings: { theme = "light" }
} = response;

Деструктуризация в параметрах функции:

function draw({ x = 0, y = 0, color = "black" } = {}) {
// ...
}
draw({ x: 10, color: "red" });

Обмен значений без временной переменной:

[a, b] = [b, a];

Ограничения:

  • Левая часть должна быть корректным шаблоном деструктуризации;
  • При несовпадении структуры — undefined (если нет значения по умолчанию);
  • Ошибки при попытке присвоить undefined или null в деструктуризацию объекта (например, const { x } = nullTypeError).

Деструктуризация — отдельный механизм, интегрированный в ядро языка. Она компилируется в последовательность операций доступа к свойствам и присваиваний, и её производительность сравнима с ручным извлечением.


Операторы расширения (...) и остатка

Синтаксис ... (три точки) используется в двух принципиально разных, но внешне похожих контекстах: как расширение (spread) и как остаток (rest). Их различие определяется положением в выражении.

1. Расширение (Spread)

Оператор расширения извлекает элементы из итерируемого объекта (массива, строки, Set, Map, генератора и т.д.) и «распыляет» их в месте использования. Контексты применения:

a) В литералах массивов
const a = [1, 2];
const b = [0, ...a, 3]; // [0, 1, 2, 3]

— создаёт новый массив, копируя элементы из a. Это мелкая копия (shallow copy).

b) В литералах объектов (начиная с ES2018)
const defaults = { theme: "light", lang: "ru" };
const config = { ...defaults, lang: "en" }; // { theme: "light", lang: "en" }

— копирует собственные перечисляемые свойства из defaults в новый объект. При совпадении ключей позже указанные свойства перекрывают более ранние.

Особенности объектного spread:

  • Не вызывает геттеры — читает значения напрямую;
  • Не копирует символы, если они не перечисляемы;
  • Не копирует прототип, не вызывает конструктор — результат всегда «плоский» Object;
  • При конфликтах — побеждает последнее свойство: { ...{a:1}, a:2 }{a:2}.
c) В вызовах функций
Math.max(...[10, 20, 30]); // эквивалентно Math.max(10, 20, 30)

— распаковывает итерируемый объект в список аргументов. Это замена Function.prototype.apply.

d) В строках и других итерируемых
[..."hello"] // ['h','e','l','l','o']
new Set(...[1, 1, 2]) // Set {1, 2}

Важные ограничения:

  • Spread работает только с итерируемыми объектами (Symbol.iterator должен быть определён). {a:1} — не итерируемый, поэтому ...{a:1} в массиве вызовет ошибку (но в объекте — разрешено, т.к. объектный spread использует другой механизм — Object.assign-подобный).
  • Spread не работает в левой части присваивания — там требуется rest.

2. Остаток (Rest)

Оператор остатка собирает неиспользованные элементы в новый массив или новый объект. Применяется в:

a) Параметрах функции
function sum(first, ...rest) {
return rest.reduce((a, b) => a + b, first);
}
sum(1, 2, 3, 4); // 10

rest получает все аргументы, начиная со второго, в виде массива.

b) Деструктуризации массива
const [head, ...tail] = [1, 2, 3, 4]; // head = 1, tail = [2, 3, 4]

tail — новый массив, содержащий оставшиеся элементы.

c) Деструктуризации объекта (ES2018+)
const { name, ...meta } = { name: "Тимур", age: 30, city: "Уфа" };
// name = "Тимур", meta = { age: 30, city: "Уфа" }

meta получает все остальные собственные перечисляемые свойства, кроме name.

Ключевые различия между spread и rest:

КритерийSpread (...x)Rest (...x)
КонтекстПравая часть, литералы, вызовыЛевая часть, параметры, деструктуризация
ДействиеРаспаковываетСобирает
РезультатМножество значений/свойствОдин массив/объект
Можно использовать в одном выражении?Да, несколько раз: [...a, ...b]Только один раз на уровень деструктуризации: [a, ...rest] — OK; [...x, ...y] — ошибка

Производительность и семантика:

  • Оба оператора создают новые структуры данных — копирование происходит при каждом использовании.
  • Spread в объектах не вызывает сеттеры — присваивание идёт напрямую.
  • Rest в объектах не включает не перечисляемые свойства и символы (если не перечисляемы).

Оператор запятой ,

Оператор запятой — один из самых редко используемых, но принципиально важных. Он вычисляет оба операнда слева направо и возвращает значение правого операнда.

let a, b;
a = (1, 2, 3); // a = 3
b = (console.log("A"), console.log("B"), 42); // выведет A, B; b = 42

Особенности:

  • Низший приоритет среди всех операторов — ниже, чем присваивание.
  • Используется в заголовках циклов for, где допустимы три выражения, разделённые запятыми:
    for (let i = 0, j = 10; i < j; i++, j--) { ... }
  • Применяется в минифицированном коде, макросах (например, в Babel-плагинах), и при необходимости выполнить побочные эффекты в выражении (например, в return, throw, yield).

Не путать с:

  • Запятой в литералах массивов/объектов — это разделитель элементов, не оператор;
  • Запятой в объявлениях переменных (let a, b) — синтаксис объявления, не выражение.

Пример использования в return:

function f() {
let x = 0;
return (x++, x * 2); // сначала инкремент, затем возврат x*2
}
f(); // 2

Хотя синтаксис корректен, чрезмерное использование оператора запятой ухудшает читаемость. Его применение оправдано только в узких технических сценариях.


Условные конструкции: if…else, switch

До сих пор мы говорили об выражениях (x ? a : b), которые возвращают значение. Теперь перейдём к инструкциям — конструкциям управления потоком, которые не возвращают значения и не могут использоваться в правой части присваивания.

1. if…else

Классическая конструкция ветвления. Синтаксис:

if (выражение) {
// блок, выполняемый если ToBoolean(выражение) === true
} else if (выражение2) {
// ...
} else {
// по умолчанию
}

Семантические детали:

  • В скобках после if должно быть выражение (любое), которое будет приведено к булеву значению.
  • Фигурные скобки { } необязательны для однострочных блоков, но их отсутствие считается антипаттерном (ошибки при добавлении строк, проблемы с else в «висячем» виде).
  • else связывается с ближайшим if, не имеющим else — что может привести к неочевидному поведению при неправильной расстановке скобок.

Рекомендации:

  • Всегда использовать фигурные скобки;
  • Избегать «висячих» else;
  • Для простых случаев — предпочитать тернарный оператор или логические операторы (если нужен результат);
  • Для множества условий — рассмотреть switch или объект-карту.

2. switch

Конструкция выбора, предназначенная для сравнения одного значения с множеством возможных вариантов.

switch (выражение) {
case значение1:
// код
break;
case значение2:
// код
break;
default:
// по умолчанию
}

Как это работает:

  1. Вычисляется выражение (селектор);
  2. Последовательно сравнивается со значениями в case через строгое равенство (===);
  3. При совпадении — начинается выполнение с этого case;
  4. Выполнение продолжается до первого break, return, throw или конца switch;
  5. Если совпадений нет — выполняется default (если есть).

Поведение без break — fall-through:

switch (status) {
case "pending":
log("В обработке");
case "approved": // ← выполнится, даже если status === "pending"
log("Одобрено");
break;
}

Это осознанная возможность — например, для объединения нескольких кейсов:

case "mon":
case "tue":
case "wed":
console.log("Рабочий день");
break;

Что можно использовать в case:

  • Любые выражения, вычисляемые в момент разбора switch (но не динамически при каждом проходе):
    const A = 1, B = 2;
    switch (x) {
    case A + B: // OK, вычисляется один раз при входе в switch
    }
  • Выражения не могут быть объектами, массивами — сравнение через === сделает их различными.

Ограничения switch:

  • Не поддерживает диапазоны (case 1..10: — нет);
  • Не поддерживает условия (case x > 5: — нет);
  • Чувствителен к типам (switch (1) { case "1": ... } не сработает).

Альтернативы:

  • Объект-карта: const handlers = { "mon": fn1, "tue": fn2 }; handlers[day]?.();
  • Массив с индексацией;
  • if…else if — для сложных условий.

Приоритеты операторов

Приоритет определяет, какие операторы вычисляются первыми в выражении без скобок. Ассоциативность — порядок вычисления операторов с одинаковым приоритетом (слева направо или справа налево).

Ниже — сокращённая, но практическая таблица приоритетов (от высшего к низшему), сгруппированная по смыслу. Полная таблица содержит 21 уровень — для повседневного использования достаточно помнить ключевые группы.

УровеньОператоры / конструкцииАссоциативностьКомментарий
21. [] () new (без аргументов)леваяДоступ к свойствам, вызов, создание
20new (с аргументами), ... (spread)правая / нетnew Foo(), ...arr
19++ -- (постфиксные)леваяx++
18++ -- (префиксные), + - ! ~ typeof void delete awaitправаяУнарные операторы
17**праваяВозведение в степень
16* / %леваяАрифметика
15+ - (бинарные)леваяСложение/вычитание (и конкатенация)
14<< >> >>>леваяПобитовые сдвиги
13< <= > >= in instanceofлеваяСравнения
12== != === !==леваяРавенство
11&леваяПобитовое И
10^леваяПобитовое XOR
9|леваяПобитовое ИЛИ
8&&леваяЛогическое И
7||леваяЛогическое ИЛИ
6??леваяНулевое слияние
5? :праваяТернарный оператор
4= += -= и все составные присваиванияправаяПрисваивание
3yield yield*праваяГенераторы
2, (оператор запятой)леваяПоследовательное вычисление
1=> (стрелочные функции)праваяТолько при многострочном теле

Практические правила чтения выражений:

  1. Сначала — вызовы и доступы: obj.method()[0].prop — сначала obj.method(), затем [0], затем .prop.
  2. Унарные — сильнее бинарных: !a + b(!a) + b, а не !(a + b).
  3. ** — правоассоциативен: 2 ** 3 ** 22 ** (3 ** 2) = 512, а не (2 ** 3) ** 2 = 64.
  4. && и || не смешиваются без скобок с ??: a || b ?? c — синтаксическая ошибка (намеренно).
  5. Тернарный — правоассоциативен: a ? b : c ? d : ea ? b : (c ? d : e).

Рекомендации:

  • Не полагайтесь на память приоритетов — используйте скобки для явного указания порядка, особенно при смешивании разных групп (==, &&, ??, ? :);
  • В IDE — включите подсветку расстановки скобок (например, в VS Code: Editor: Bracket Pair Colorization);
  • При рефакторинге — не удаляйте «лишние» скобки без проверки семантики.