Суть Прочитай четыре коротких JS-сниппета и предскажи, что рантайм делает на самом деле: округление float, предел safe integer, тихий coercion и quirk typeof null.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 14 min
Все сюрпризы повседневного кода восходят к основной идее юнита: биты сохранены под одним правилом типа, а позже прочитаны под другим. Прочитай каждый сниппет, предскажи точный вывод и назови правило, которое его породило.
Цель
Отработай цикл, который запускаешь всякий раз, когда программа печатает неожиданное: определи, какое правило типа рантайм применил к битам, и объясни, почему вывод именно такой, а не тот, что ты сперва предположил.
Heads-up Двоичный float не может хранить 0.1 или 0.2 точно, как десятичная система не хранит 1/3 точно. Хранимые значения уже слегка смещены, поэтому сумма — 0.30000000000000004, а равенство — false.
Heads-up Float складывается нормально; операция не падает. Загвоздка в точности: результат IEEE 754 — 0.30000000000000004, а не 0.3, так что удивляет лишь напечатанное значение.
Heads-up JS хранит IEEE 754 double, который сам по себе неточен для 0.1 и 0.2 — он не держит точную десятичную за кулисами. Хранимое значение действительно 0.30000000000000004.
Сниппет 2 — целое, которое перестаёт считать
const big = 9007199254740991; // Number.MAX_SAFE_INTEGERconsole.log(big + 1); // ?console.log(big + 1 === big + 2); // ?
Викторина
Completed
Что это печатает и в чём глубинная причина?
Heads-up За пределами MAX_SAFE_INTEGER 52 бита мантиссы double не могут разделить соседние целые, поэтому big + 1 и big + 2 схлопываются в одно значение и сравнение даёт true, а не false.
Heads-up number в JS не оборачивается; у единого типа double нет фиксированной целочисленной ширины, чтобы переполниться. Вместо этого значение теряет точность — большие целые округляются к ближайшему представимому double.
Heads-up Значение хранится нормально — double достигает ~1.8e308. Выше 2^53 ломается целочисленная точность: результат молча округляется, а не отвергается. Для точных больших целых используй BigInt.
Что печатают три строки и какое правило объясняет различие?
Heads-up Оператор + перегружен: если одна из сторон — строка, он конкатенирует, поэтому 5 + '5' — это '55', а не 10. Лишь - и * (без строкового смысла) форсируют числовой coercion.
Heads-up JS динамически типизирован и здесь приводит, а не бросает. Он молча конвертирует операнды, чтобы каждый оператор сработал — именно поэтому неверный тип может дать неверное значение без ошибки.
Heads-up Строки побеждают лишь для +. Для - и *, у которых нет строкового определения, JS приводит строку к числу, давая 0 и 10. Направление coercion решает оператор.
Что печатает каждая строка и какой результат — исторический quirk?
Heads-up typeof null не возвращает 'null'. Из-за бага в исходной реализации он возвращает 'object', и это нельзя исправить, не сломав веб. Надёжная проверка — x === null.
Heads-up null === undefined — это false: строгое равенство сравнивает значение и тип, а это два различных примитива. Они равны лишь нестрого под ==.
Heads-up Это различные примитивы с различными результатами typeof: 'object' для null (quirk) и 'undefined' для undefined. Под === они не равны.
Итог
Каждый сюрприз здесь — та же идея юнита в костюме JS. Округление float (0.1 + 0.2) и предел safe integer (выше 2^53) оба идут от единого 64-битного IEEE 754 double с конечной точностью — отдельного целочисленного типа, к которому можно отступить, нет. Оператор + конкатенирует со строками, но приводит к числам для - и *, так что динамическая типизация молча выбирает правило типа за тебя. А typeof null возвращает ‘object’ — замороженный исторический баг. Прочитай оператор и формат хранения — и вывод перестаёт быть загадкой.