Навигация

Типы данных и приведение типов

Прежде чем говорить о замыканиях, this или Event Loop, нужно разобраться, из чего состоят значения в JavaScript. Типы данных и правила их преобразования — это фундамент, на котором строится всё остальное. Именно здесь чаще всего скрываются «странные» баги и собеседовательские ловушки.

Содержание

  1. Примитивы и объекты
  2. Стек и куча
  3. typeof и его особенности
  4. Явное приведение типов
  5. Неявное преобразование (coercion)
  6. == vs ===, Object.is, Number.isNaN
  7. Собеседовательские ловушки
  8. Ключевые тезисы для интервью
  9. Заключение
  10. Полезные ссылки

Примитивы и объекты

Все значения в 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

Семь примитивных типов

  1. number — числа (целые, дробные, NaN, Infinity, -Infinity).
  2. bigint — целые числа произвольной длины (123n).
  3. string — строки.
  4. boolean — true / false.
  5. undefined — значение не присвоено.
  6. null — явное «ничего».
  7. 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.

Алгоритм пытается получить примитив в таком порядке:

  1. Для hint “string”: сначала toString(), затем valueOf().
  2. Для 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
  1. false — булево, преобразуется в число: Number(false) → 0.
  2. [] — объект, hint “default”. Вызывается valueOf() — возвращает сам массив (не примитив).
  3. Тогда вызывается toString() — пустой массив даёт пустую строку ''.
  4. Пустая строка преобразуется в число: Number('') → 0.
  5. Сравниваем: 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:

  • false
  • 0
  • -0
  • 0n (bigint)
  • '' (пустая строка)
  • null
  • undefined
  • NaN

Всё остальное — 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 при работе с дробями. Потренируйтесь предсказывать результаты приведения типов для разных выражений — это навык, который быстро становится автоматическим.

Полезные ссылки