Типы данных и приведение типов
Прежде чем говорить о замыканиях, this или Event Loop, нужно разобраться, из чего состоят значения в JavaScript. Типы данных и правила их преобразования — это фундамент, на котором строится всё остальное. Именно здесь чаще всего скрываются «странные» баги и собеседовательские ловушки.
Содержание
- Примитивы и объекты
- Стек и куча
typeofи его особенности- Явное приведение типов
- Неявное преобразование (coercion)
==vs===,Object.is,Number.isNaN- Собеседовательские ловушки
- Ключевые тезисы для интервью
- Заключение
- Полезные ссылки
Примитивы и объекты
Все значения в JavaScript делятся на две категории: примитивы и объекты.
Примитивы хранятся по значению. Когда вы присваиваете примитив другой переменной, создаётся независимая копия:
let a = 10;
let b = a;
b = 20;
console.log(a); // 10
console.log(b); // 20
Объекты (включая массивы, функции, даты, регулярки) хранятся по ссылке. Переменная указывает на область памяти, где лежит сам объект:
let arr1 = [1, 2, 3];
let arr2 = arr1;
arr2.push(4);
console.log(arr1); // [1, 2, 3, 4]
console.log(arr2); // [1, 2, 3, 4]
Обе переменные ссылаются на один и тот же массив. Поэтому сравнение объектов проверяет не содержимое, а идентичность ссылок:
console.log({} === {}); // false
console.log([] === []); // false
let obj = {};
let ref = obj;
console.log(obj === ref); // true
Семь примитивных типов
number— числа (целые, дробные,NaN,Infinity,-Infinity).bigint— целые числа произвольной длины (123n).string— строки.boolean—true/false.undefined— значение не присвоено.null— явное «ничего».symbol— уникальный идентификатор.
Все остальное — объекты, в том числе массивы и функции. Функции — это «вызываемые объекты» со специальным внутренним поведением.
symbol и bigint чуть подробнее
Symbol создаёт уникальное значение, которое нельзя случайно переопределить. Символы часто используются для «скрытых» ключей объекта:
const id = Symbol('id');
const user = { name: 'Alice', [id]: 123 };
console.log(user[id]); // 123
console.log(Object.keys(user)); // ['name'] — символ не попадает в перечисление
У символов есть « well-known » константы: Symbol.iterator, Symbol.toPrimitive, Symbol.hasInstance и другие. Они позволяют менять поведение объекта при итерации, приведении типов и т.д.
bigint нужен для чисел, которые не помещаются в number (больше Number.MAX_SAFE_INTEGER):
const huge = 9007199254740993n;
console.log(huge + 1n); // 9007199254740994n
bigint и number нельзя смешивать арифметически — 1n + 1 выбросит TypeError.
Стек и куча
Под капотом примитивы хранятся в стеке — быстрой, но ограниченной памяти. Объекты хранятся в куче — большой области памяти, доступ к которой медленнее. Переменная-объект в стеке содержит лишь ссылку на кучу.
Это объясняет, почему:
- примитивы неизменяемы (immutable):
let s = 'abc'; s.toUpperCase()вернёт новую строку, аsостанется прежней; - объекты изменяемы (mutable): меняя массив через одну ссылку, вы меняете объект для всех ссылок;
- сравнение объектов через
===проверяет ссылки, а не содержимое.
Если нужно сравнить содержимое объектов, придётся делать это вручную — например, через глубокое сравнение или сериализацию.
typeof и его особенности
Оператор typeof возвращает строку с именем типа:
typeof 42; // "number"
typeof 42n; // "bigint"
typeof 'hello'; // "string"
typeof true; // "boolean"
typeof undefined; // "undefined"
typeof null; // "object" ← исторический баг
typeof {}; // "object"
typeof []; // "object"
typeof function() {}; // "function"
typeof Symbol(); // "symbol"
Почему typeof null === 'object'? В первой реализации JavaScript тип определялся по младшим битам значения. У null все биты типа были нулями, что соответствовало типу «объект». Исправление сломало бы огромное количество существующего кода, поэтому баг остался навсегда.
Чтобы надёжно проверить null, используйте строгое сравнение:
value === null;
Чтобы отличить массив от обычного объекта, лучше Array.isArray(arr):
Array.isArray([]); // true
Array.isArray({}); // false
Явное приведение типов
Вместо того чтобы полагаться на неявные правила JavaScript, преобразуйте типы явно — так код становится предсказуемее.
В число
Number('42'); // 42
Number('3.14'); // 3.14
Number(''); // 0
Number(null); // 0
Number(undefined); // NaN
Number(true); // 1
Number('12px'); // NaN
parseInt и parseFloat читают число с начала строки, пока могут:
parseInt('12px'); // 12
parseInt('FF', 16); // 255
parseFloat('3.14abc'); // 3.14
parseInt('abc'); // NaN
Совет: всегда указывайте radix у parseInt, чтобы избежать сюрпризов со старыми браузерами: parseInt('08', 10).
В строку
String(42); // "42"
String(null); // "null"
String(undefined); // "undefined"
String(true); // "true"
В булево
Boolean(1); // true
Boolean(0); // false
Boolean(''); // false
Boolean('hello'); // true
Boolean(null); // false
Boolean([]); // true ← пустой массив truthy
Boolean({}); // true ← пустой объект truthy
Object.is и Number.isNaN
Object.is работает почти как ===, но с двумя важными отличиями:
Object.is(NaN, NaN); // true
Object.is(+0, -0); // false
NaN === NaN; // false
+0 === -0; // true
Number.isNaN проверяет именно числовое NaN, в отличие от глобального isNaN, который сначала пытается преобразовать аргумент в число:
Number.isNaN(NaN); // true
Number.isNaN('NaN'); // false
isNaN('NaN'); // true ← преобразовал строку в NaN
Неявное преобразование (coercion)
JavaScript автоматически преобразует типы в определённых контекстах. Это удобно, но опасно, если не понимать правил.
Строковое преобразование через +
Если хотя бы один операнд — строка, оператор + выполняет конкатенацию:
5 + '5'; // "55"
true + '5'; // "true5"
null + '5'; // "null5"
Числовое преобразование через -, *, /, %
'5' - 3; // 2
'5' * 2; // 10
'5' / 2; // 2.5
'hello' - 3; // NaN
null + 5; // 5 ← null → 0
undefined + 5; // NaN ← undefined → NaN
Булево преобразование в условиях
if ('hello') { /* выполнится */ }
if (0) { /* не выполнится */ }
if ([]) { /* выполнится */ }
if ({}) { /* выполнится */ }
Преобразование объектов в примитивы (ToPrimitive)
Когда объект участвует в операции, требующей примитива (сравнение, арифметика, конкатенация), JavaScript вызывает внутренний алгоритм ToPrimitive. Он определяет «подсказку» (hint) — какой тип ожидается:
- hint “string” — для
String(obj), конкатенации через+с строкой,alert(obj). - hint “number” — для арифметики (
-,*,/),Number(obj), сравнений<,>. - hint “default” — для
==,+без явного строкового операнда,new Date() == value.
Алгоритм пытается получить примитив в таком порядке:
- Для hint “string”: сначала
toString(), затемvalueOf(). - Для hint “number” и “default”: сначала
valueOf(), затемtoString().
Если ни один метод не вернул примитив — ошибка TypeError.
const obj = {
toString() { return 'hello'; },
valueOf() { return 42; }
};
console.log(String(obj)); // "hello" (hint "string" → toString)
console.log(Number(obj)); // 42 (hint "number" → valueOf)
console.log(obj + ''); // "42" (hint "default" → valueOf → 42, затем строка)
console.log(obj + 10); // 52 (hint "default" → valueOf → 42, затем число)
Почему [] == false даёт true
Разберём по шагам:
[] == false; // true
false— булево, преобразуется в число:Number(false)→0.[]— объект, hint “default”. ВызываетсяvalueOf()— возвращает сам массив (не примитив).- Тогда вызывается
toString()— пустой массив даёт пустую строку''. - Пустая строка преобразуется в число:
Number('')→0. - Сравниваем:
0 == 0→true.
Аналогично для [] == 0:
[] == 0; // true
// [] → '' → 0, затем 0 == 0
Symbol.toPrimitive — переопределение поведения
Метод Symbol.toPrimitive позволяет явно указать, как объект преобразуется в примитив для каждого hint:
const money = {
amount: 100,
[Symbol.toPrimitive](hint) {
console.log(`hint: ${hint}`);
if (hint === 'string') return `$${this.amount}`;
if (hint === 'number') return this.amount;
return this.amount; // default
}
};
console.log(String(money)); // "hint: string" → "$100"
console.log(Number(money)); // "hint: number" → 100
console.log(money + ''); // "hint: default" → "100"
console.log(money * 2); // "hint: number" → 200
Если определён Symbol.toPrimitive, он имеет приоритет над valueOf и toString.
valueOf и toString — legacy-методы
До появления Symbol.toPrimitive объекты полагались на valueOf и toString. Они всё ещё работают:
const point = {
x: 3,
y: 4,
valueOf() {
return Math.sqrt(this.x ** 2 + this.y ** 2); // 5
},
toString() {
return `(${this.x}, ${this.y})`;
}
};
console.log(Number(point)); // 5 (valueOf)
console.log(String(point)); // "(3, 4)" (toString)
console.log(point + 10); // 15 (valueOf → 5, затем +10)
console.log(`${point}`); // "(3, 4)" (template literal → toString)
Важно: toString вызывается автоматически при интерполяции в template literal (${obj}) — hint “string”.
Falsy и truthy
Falsy — значения, которые преобразуются в false:
false0-00n(bigint)''(пустая строка)nullundefinedNaN
Всё остальное — truthy, включая '0', 'false', [], {}, new Date().
== vs ===, Object.is, Number.isNaN
Строгое сравнение === сравнивает и тип, и значение. Преобразования не происходит:
5 === '5'; // false
null === undefined; // false
0 === false; // false
Нестрогое сравнение == сначала пытается привести типы:
5 == '5'; // true
null == undefined; // true
0 == false; // true
'' == false; // true
[] == false; // true
Последний пример: [] приводится к пустой строке '', затем к 0, и 0 == false даёт true.
Практическое правило: почти всегда используйте ===. Исключение — проверка на null или undefined одновременно:
if (value == null) {
// value === null || value === undefined
}
Для проверки NaN используйте Number.isNaN или Object.is.
Собеседовательские ловушки
[] + {} и {} + []
console.log([] + {}); // "[object Object]"
console.log(({}) + []); // "[object Object]"
Здесь [] превращается в пустую строку, а {} — в строку "[object Object]". Результат конкатенации — "[object Object]".
А вот такой код в браузере даст 0:
{} + []; // 0
Почему? Потому что {} в начале инструкции интерпретируется как пустой блок кода, а не как объект. Остаётся +[], что приводит пустой массив к числу 0. Если {} находится в позиции выражения (например, внутри console.log или в скобках), он становится объектным литералом и результат уже "[object Object]".
0.1 + 0.2
console.log(0.1 + 0.2); // 0.30000000000000004
console.log(0.1 + 0.2 === 0.3); // false
Числа в JavaScript хранятся в формате IEEE 754 (64 бита). Дроби в десятичной системе часто не имеют точного двоичного представления. Для сравнения дробей используйте допуск:
function equal(a, b, eps = Number.EPSILON) {
return Math.abs(a - b) < eps;
}
console.log(equal(0.1 + 0.2, 0.3)); // true
typeof NaN
typeof NaN; // "number"
NaN — это специальное числовое значение «not-a-number», поэтому его тип всё равно number.
Ключевые тезисы для интервью
- Примитивы хранятся по значению в стеке, объекты — по ссылке в куче.
typeof null === 'object'— это исторический баг первой реализации JavaScript, который остался навсегда.- Falsy-значения:
false,0,-0,0n,'',null,undefined,NaN; всё остальное — truthy, включая[]и{}. ==допустимо только для проверкиvalue == null; в остальных случаях используйте===.[] + {}даёт"[object Object]", потому что массив приводится к'', а объект — к"[object Object]".- Явное приведение типов:
Number(),String(),Boolean(),parseInt(),parseFloat(). Object.isотличается от===в двух случаях:Object.is(NaN, NaN)→trueиObject.is(+0, -0)→false.0.1 + 0.2 !== 0.3из-за неточного двоичного представления дробей в формате IEEE 754.- Объекты преобразуются в примитивы через алгоритм ToPrimitive с hint “string”, “number” или “default”.
Symbol.toPrimitiveимеет приоритет надvalueOfиtoStringпри приведении объектов.[] == falseдаётtrue, потому что[]→''→0, иfalse→0; результат:0 == 0.
Заключение
Типы данных и приведение типов — основа JavaScript. Понимание различий между примитивами и объектами, правил неявного преобразования и механизмов сравнения позволяет избегать скрытых багов и уверенно проходить собеседования. Всегда приводите типы явно, используйте === вместо == и помните про особенности IEEE 754 при работе с дробями. Потренируйтесь предсказывать результаты приведения типов для разных выражений — это навык, который быстро становится автоматическим.