В JavaScript у значения есть тип, а у переменной нет. Одна и та же переменная может хранить число, потом строку. Отсюда половина «странностей» языка: операторы сами приводят значения к нужному типу, и эти правила надо знать наизусть.
Восемь типов
Семь примитивов и объекты:
| Тип | Пример | typeof |
|---|---|---|
| undefined | undefined | "undefined" |
| null | null | "object" (историческая ошибка) |
| boolean | true | "boolean" |
| number | 42, NaN, Infinity | "number" |
| bigint | 42n | "bigint" |
| string | 'abc' | "string" |
| symbol | Symbol('id') | "symbol" |
| object | {}, [], функции, даты | "object", для функций "function" |
Примитивы неизменяемы и сравниваются по значению. Объекты сравниваются по ссылке: {} === {} это false, потому что это два разных объекта.
Когда вы пишете 'abc'.length, движок на мгновение оборачивает примитив в объект-обёртку String, читает свойство и выбрасывает обёртку. Поэтому у примитивов есть методы, но записать им свойство нельзя: s.x = 1 молча потеряется (в строгом режиме упадёт с TypeError).
Как точно узнать тип
typeofхорош для примитивов и функций, ноtypeof null === 'object'иtypeof [] === 'object'.value === nullпроверяет null.Array.isArray(value)проверяет массив, в том числе пришедший из другого iframe.Object.prototype.toString.call(value)даёт[object Date],[object RegExp]и так далее.Number.isNaN(x)проверяет NaN без приведения. ГлобальныйisNaN('abc')сначала приводит строку к числу и вернётtrue.
Три вида приведения
Любое неявное приведение сводится к трём операциям: к строке, к числу, к булеву.
К булеву. Ложных значений ровно восемь: false, 0, -0, 0n, '', null, undefined, NaN. Всё остальное истинно, включая '0', 'false', [] и {}.
К числу. Number('') и Number(null) дают 0, Number(undefined) даёт NaN, Number('12px') даёт NaN, Number(true) даёт 1, Number([]) даёт 0, Number([5]) даёт 5. Пробелы по краям строки игнорируются: Number(' 7 ') это 7.
К строке. String(null) это 'null', String([1, 2]) это '1,2', String({}) это '[object Object]'.
Объект сначала превращается в примитив: движок вызывает Symbol.toPrimitive, если он есть, иначе valueOf и toString (порядок зависит от подсказки: для чисел сначала valueOf, для строк сначала toString). У обычных объектов и массивов valueOf возвращает сам объект, поэтому в ход идёт toString.
const price = {
valueOf() { return 100; },
toString() { return 'сто'; },
};
console.log(price * 2); // 200
console.log(`${price}`); // 'сто'
Плюс и остальные операторы
Бинарный + особенный: если после приведения к примитиву хотя бы один операнд строка, выполняется конкатенация. Остальные арифметические операторы всегда приводят к числу.
1 + '2' // '12'
'5' - 2 // 3
'5' * '2' // 10
true + 1 // 2
[] + 1 // '1' ([] -> '')
null + 1 // 1
undefined + 1 // NaN
1 + 2 + '3' // '33' (слева направо: 3 + '3')
'1' + 2 + 3 // '123'
Унарный плюс +x это короткая запись Number(x).
== против ===
=== сравнивает без приведения: разные типы сразу дают false. == сначала приводит:
null == undefinedэтоtrue, и больше ни с чем они нестрого не равны (null == 0даётfalse).- Если один операнд число, а другой строка, строка приводится к числу.
- Булево всегда сначала превращается в число:
true == '1'даётtrue, аtrue == 'true'даётfalse(1 == NaN). - Объект против примитива: объект приводится к примитиву.
NaNне равен ничему, даже себе.
Отдельно Object.is: как ===, но Object.is(NaN, NaN) это true, а Object.is(0, -0) это false.
Правило для работы: всегда ===. Единственное частое исключение: x == null ловит сразу null и undefined.
Сравнения < и >
Если оба операнда строки, они сравниваются посимвольно по кодам: '10' < '9' даёт true. Иначе оба приводятся к числу: '10' < 9 даёт false. Отсюда же ловушка null >= 0 это true, хотя null == 0 это false: сравнение приводит null к 0, а == для null особое.
Числа
Все number это 64-битные числа с плавающей точкой. Поэтому 0.1 + 0.2 !== 0.3, а целые точны только до Number.MAX_SAFE_INTEGER (2^53 - 1). Для больших целых есть bigint, но смешивать его с number в арифметике нельзя: 1n + 1 бросит TypeError.
parseInt('12px') читает число из начала строки и даёт 12, а Number('12px') даёт NaN. Передавайте parseInt основание: parseInt('08', 10).
Частые ошибки
- Проверка
if (count)там, где 0 это нормальное значение. Нужноcount !== undefinedили??. typeof x === 'object'как проверка на объект: пропуститnull.- Сложение значений из
input.value: это строки,'2' + '3'даст'23'. arr.indexOf(NaN)всегда-1, аarr.includes(NaN)находит NaN.
Что спросят на собесе
- Перечислите типы данных и почему
typeof nullравен'object'. - Чем
==отличается от===, когда допустимо==. - Какие значения ложные, во что превращаются
[],{},null,undefinedпри приведении к числу и строке. - Почему
0.1 + 0.2 !== 0.3и как сравнивать дробные числа. - Как надёжно проверить, что значение массив, NaN или null.