Раздел 3 · JavaScript

Синтаксис, типы и приведение

junior~45 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Синтаксис, типы и приведение

JavaScript изначально кажется простым языком: можно написать почти что угодно, и оно как-нибудь да заработает. Деление на ноль не падает, а возвращает Infinity. Сравнение 1 == "1" оказывается правдой. [] + {} тихо превращается в строку, null + 1 отдаёт единицу, а typeof null упорно показывает 'object'. Там, где другие языки кричат об ошибке компиляции, JS пожимает плечами и выдаёт результат, не всегда тот, который ты ожидал.

Эта снисходительность языка и есть главная ловушка. Пока ты пишешь на JS по привычкам из C, Python или Go, рассчитывая, что компилятор поймает опечатки, JS будет молча соглашаться и подкидывать тихие сюрпризы. 0.1 + 0.2 не равно 0.3. Строка '10' “меньше” строки '9'. Пустой массив истинный при приведении к булеву, но равен нулю через ==. Это не баги, всё это документированное поведение, описанное в спецификации. Просто оно требует, чтобы ты знал, как именно язык читает выражения и как один тип превращается в другой.

Этот урок собирает базис. Мы пройдём по семи примитивам и научимся отличать их от объектов, разберём IEEE 754 и поймём, откуда берутся хвостовые ошибки в дробных числах, посмотрим, что именно возвращают &&, || и ??, чем == отличается от === и когда нужен Object.is, разложим по полочкам for...of, for...in и switch. Этого достаточно, чтобы перестать гадать и начать читать чужой код буквально.

Дальше серия достраивает остальное: функции и замыкания во втором уроке, объекты и прототипы в третьем, ФП-стиль в четвёртом, коллекции и итерационные протоколы в пятом, строки и Unicode в шестом, ошибки и события в седьмом, метапрограммирование и V8 в восьмом.

1. Откуда брать JS

Минимум три способа запустить любой пример из урока.

# 1) Node REPL (Active LTS, на 2026 это 22.x)
$ node
> 1 + 2
3

# 2) одной строкой
$ node -e 'console.log(0.1 + 0.2)'
0.30000000000000004

# 3) файлом
$ echo 'console.log(typeof null)' > scratch.js
$ node scratch.js
object

В браузере открой DevTools (Cmd+Opt+I в Chrome) и пиши в Console. Поведение ровно то же, кроме того, что в браузере есть window, document, localStorage, а в Node: process, Buffer, require/import. Браузерный API трогаем в разделе 07, серверный, в разделе 06.

2. Семь примитивов

Всё, что не объект, в JS примитив. Их семь:

ТипЛитералtypeof
number42, 0.1, Infinity, NaNnumber
bigint42n, 2n ** 64nbigint
string'hi', "hi", `hi`string
booleantrue, falseboolean
symbolSymbol('id')symbol
nullnullobject (историческая ошибка)
undefinedundefinedundefined

Объект не примитив. Массив, функция, дата, регулярка, Map, Set, всё это объекты с typeof === 'object' (функция отдельно, у неё typeof === 'function').

typeof 42;          // 'number'
typeof 42n;         // 'bigint'
typeof 'hi';        // 'string'
typeof true;        // 'boolean'
typeof Symbol();    // 'symbol'
typeof null;        // 'object'  <- баг 1995 года, пофиксить уже нельзя
typeof undefined;   // 'undefined'
typeof {};          // 'object'
typeof [];          // 'object'
typeof (() => {});  // 'function'

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

Примитивы передаются по значению, объекты по ссылке. Подробнее в уроке 3 (03-objects-arrays-prototypes), пока запомни: присваивание примитива копирует, присваивание объекта копирует только ссылку.

let a = 1;
let b = a;
b = 2;
console.log(a); // 1, копия

const o1 = { x: 1 };
const o2 = o1;
o2.x = 2;
console.log(o1.x); // 2, та же ссылка

3. Number и IEEE 754

В JS только один числовой тип, IEEE 754 double. Целые до 2 ** 53 - 1 представлены точно, дальше пропуски. Дробные с конечной двоичной записью точны (0.5, 0.25, 0.125), остальные, бесконечные дроби в двоичной системе, как 0.1 или 0.2.

0.1 + 0.2 === 0.3;          // false
0.1 + 0.2;                  // 0.30000000000000004
Number.MAX_SAFE_INTEGER;    // 9007199254740991
2 ** 53 === 2 ** 53 + 1;    // true, точность кончилась

Поэтому в финансах и идентификаторах храним целые в копейках/центах или используем BigInt:

const orderId = 9007199254740993n; // ровно столько, без потерь
orderId + 1n;                      // 9007199254740994n
orderId + 1;                       // TypeError: Cannot mix BigInt and other types
Number(orderId);                   // 9007199254740992 (потеряли единицу)

Сравнение дробных через эпсилон:

const equalsApprox = (a, b, eps = Number.EPSILON) =>
  Math.abs(a - b) < eps;

equalsApprox(0.1 + 0.2, 0.3); // true

NaN, особый случай:

NaN === NaN;             // false, единственное значение, не равное себе
Number.isNaN(NaN);        // true
isNaN('abc');             // true (приводит к Number, потом проверяет)
Number.isNaN('abc');      // false (никаких приведений)

Запомни: Number.isNaN без приведения, isNaN глобальный с приведением. Используй Number.isNaN, Number.isFinite, Number.isInteger, чтобы не нарваться на скрытое приведение.

4. String, кратко

Строка в JS, последовательность UTF-16 кодовых единиц. Это значит, что "😀".length === 2, потому что emoji занимает две кодовые единицы (surrogate pair). Подробно про Unicode и Intl в уроке 6 (06-strings-regex-unicode). Пока минимум:

'a' + 'b';           // 'ab'
`x = ${1 + 2}`;       // 'x = 3', шаблонная строка
'abc'.length;         // 3
'😀'.length;          // 2
[...'😀'].length;     // 1, итерация по code points

Литералы взаимозаменяемы, ', ", `. Шаблонные литералы дают интерполяцию ${expr} и многострочность. Конкатенация через + работает, но в новом коде используй шаблонные.

дальше после входа

Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.

Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.