Число — примитивный тип Number в JavaScript. Под одним именем тут живут и целые (42, -7), и дробные (3.14), и шестнадцатеричные (0xff), и экспоненциальные (1e3). Технически все они хранятся в одном формате — 64-битное число с плавающей точкой по стандарту IEEE 754. Из-за этого, например, 0.1 + 0.2 возвращает не 0.3, а 0.30000000000000004, — но это уже отдельная история.

let count = 42;
let price = 1999.99;
let hex = 0xff;        // 255
let exp = 1e3;         // 1000
let negative = -273.15;

В чистом виде число встречается реже, чем кажется — в реальном коде оно почти всегда либо приходит откуда-то снаружи, либо уходит куда-то наружу. И вот тут начинаются преобразования.

Откуда в коде появляются числа в «неправильной» форме

Несколько типичных мест, где значение приходит как число, но не является числом с точки зрения JavaScript:

  • Формы. Любой <input type="number"> возвращает в JS строку. То есть document.querySelector('input').value — это "42", а не 42. Если сложить такую «цифру» с другой через +, получится конкатенация: "42" + "10" === "4210".
  • URL и query-параметры. URLSearchParams.get('page') отдаёт "3", не 3.
  • localStorage / sessionStorage. Хранилища знают только строки. Положил число — забрал строку.
  • Старые API. Некоторые JSON-ответы (особенно от старых бэкендов) присылают цифры в кавычках, чтобы избежать потери точности при больших значениях вроде ID-шников.
  • CSS-значения через JS. getComputedStyle(el).width вернёт "320px" — чтобы посчитать что-то, придётся распарсить.

А ещё число постоянно нужно конвертировать в другую сторону — например, в строку перед записью в DOM или в булево для проверки в if.

Все пять направлений преобразования

Из числа можно сделать любой другой тип. Каждый случай разобран в отдельной главе — здесь короткий обзор, чтобы понимать, что куда ведёт.

  • Число в строку — самый частый случай: вывод в шаблон, конкатенация, форматирование. Способов три: String(num), num.toString(), `${num}`. Внутри они почти равны, но toString() ещё умеет брать систему счисления вторым аргументом — (255).toString(16) вернёт "ff".
  • Число в булевоеBoolean(num) или короткое !!num. Тут единственная странность: NaN считается false, как и 0 с -0. Всё остальное (включая Infinity) — true.
  • Число в массив — задача обычно сводится к «посчитать цифры в числе»: [...String(num)] или String(num).split(''). Если нужны именно цифры как числа, в конце ещё .map(Number).
  • Число в объект — редкий кейс, в основном для оборачивания через new Number(num) (объект-обёртка). В современном коде почти не нужен — примитивы и так автоматически «боксятся» при вызове методов вроде (42).toFixed(2).
  • Число в специальное значениеnull, undefined, NaN. Делается тернарником или if: когда число логически «пустое» или «неизвестное».

Явное и неявное преобразование — и почему первое лучше

JavaScript часто приводит числа автоматически — это называется неявным (или имплицитным) преобразованием. Например, оператор + в выражении "3" + 1 сложит как строки и вернёт "31". А "3" - 1 — уже как числа, вернёт 2. Логично? Не очень.

"3" + 1   // "31"   (плюс: конкатенация)
"3" - 1   // 2      (минус: вычитание)
"3" * 2   // 6      (звёздочка: умножение)
"3" / 1   // 3      (слэш: деление)
+"3"      // 3      (унарный плюс — каст в число)
"5" == 5  // true   (== делает каст; === не делает)

Эти приведения короткие и порой удобные, но в команде их быстро забывают и читают неправильно. Лучше писать явно: Number(x), parseInt(x, 10), parseFloat(x). Тогда соседу по коду не приходится разгадывать, какое преобразование тут произойдёт.

Маленький лайфхак: если нужно превратить строку в число и не важно, целое или дробное, — пиши Number(x). Если важно отрезать единицы измерения ("320px"), — parseInt(x, 10). Второй аргумент 10 — основание системы счисления; без него parseInt может сюрпризно интерпретировать "0x10" как шестнадцатеричное.

Главные подводные камни

Несколько граблей, на которые наступали все, кто работал с числами в JS.

NaN — это число. Самое странное: typeof NaN === "number". Это «не число», которое числом и является. NaN не равен сам себе (NaN === NaNfalse), и любая арифметика с ним даёт снова NaN. Проверять надо через Number.isNaN(x) — именно эту функцию, а не глобальную isNaN(): глобальная сначала приводит аргумент к числу, и isNaN("hello") вернёт true, что обычно не то, чего ожидаешь.

Number("hello")     // NaN
Number("")          // 0      (пустая строка → ноль, не NaN)
Number(null)        // 0      (null → ноль; внезапно)
Number(undefined)   // NaN    (undefined → не число)
Number(true)        // 1
Number(false)       // 0
Number([])          // 0      (пустой массив → ноль)
Number([42])        // 42     (массив из одного элемента → этот элемент)
Number([1, 2])      // NaN    (массив с двумя → NaN)

Большие целые — не всегда точные. Безопасный диапазон целых чисел — от -(2⁷³ - 1) до 2⁷³ - 1 (от Number.MIN_SAFE_INTEGER до Number.MAX_SAFE_INTEGER). Если ID базы пришёл больше — используй BigInt, не Number.

Числа с ведущим нулём. parseInt("08") в современных движках вернёт 8, а в очень старых — 0 (восьмеричная система). Чтобы не было сюрпризов на легаси, всегда указывай второй аргумент: parseInt("08", 10).