Все о значениях в языке JavaScript: от типов до глубокого копирования
Основные понятия о значениях в JavaScript
Главное различие: примитивные и ссылочные типы
Все значения в JavaScript делятся на две категории: примитивы (числа, строки, булевы, null, undefined, Symbol, BigInt) и объекты (включая массивы, функции, даты). Примитивы хранятся и передаются по значению, объекты - по ссылке. Это фундаментальное поведение определяет работу с переменными, сравнение и копирование данных.
let a = 10;
let b = a; // копируется значение
b = 20;
console.log(a); // 10значения js (значения в javascript)
let obj1 = { key: 'value' };
let obj2 = obj1; // копируется ссылка
obj2.key = 'new';
console.log(obj1.key); // 'new'Понимание этого механизма помогает избежать неожиданных изменений данных. Для копирования объектов требуется явное создание нового объекта (поверхностное или глубокое).
Как определить тип значения?
Оператор typeof возвращает строку с типом, но имеет особенности: typeof null === 'object' (историческая ошибка), typeof [] === 'object', typeof NaN === 'number'. Для точной проверки массивов используется Array.isArray(), для null - value === null, для NaN - Number.isNaN(). Более универсальный способ - Object.prototype.toString.call(value).
console.log(typeof 'hello'); // 'string'
console.log(typeof true); // 'boolean'
console.log(typeof undefined); // 'undefined'
console.log(typeof null); // 'object' (ошибка)
console.log(typeof [1,2]); // 'object'
// Точная проверка null
console.log(null === null); // true
// Проверка массива
console.log(Array.isArray([1,2])); // true
// Универсальный способ для встроенных типов
console.log(Object.prototype.toString.call([])); // '[object Array]'
console.log(Object.prototype.toString.call(new Date())); // '[object Date]'Типичные ошибки:
- Ожидание
typeof null === 'null' - Путаница между
NaNи обычным числом (необходимоNumber.isNaN(), так какNaN !== NaN)
Как преобразовать значение в другой тип?
Явное преобразование выполняется функциями String(), Number(), Boolean(). Неявное происходит при операциях сложения строк, сравнения, логических контекстах. Проблемы: Number('1.2.3') вернёт NaN, String(null) === 'null', а Boolean('false') === true (непустая строка истинна).
// Явное преобразование
console.log(Number('42')); // 42
console.log(String(123)); // '123'
console.log(Boolean(0)); // false
console.log(Boolean('')); // false
// Неявное (особенности)
console.log('5' - 2); // 3 (строка преобразуется в число)
console.log('5' + 2); // '52' (число преобразуется в строку)
console.log(!!null); // false
// Проблемные случаи
console.log(Number('2a')); // NaN
console.log(Boolean('false')); // trueТипичные ошибки:
- Неявное сложение строки и числа даёт конкатенацию
- Преобразование пустого массива в число:
Number([]) === 0, а в строку:String([]) === '' - Использование
!!valueдля приведения к булеву типу - не всегда очевидно (например,!!'0' === true)
Как правильно сравнивать значения?
Оператор == выполняет неявное преобразование типов, === строго сравнивает без преобразования. Object.is() работает как ===, но исправляет два особых случая: Object.is(NaN, NaN) === true, Object.is(0, -0) === false. Для сравнения объектов по содержимому (глубокое сравнение) требуется рекурсивная проверка или библиотека.
// Строгое vs нестрогое
console.log('5' == 5); // true
console.log('5' === 5); // false
console.log(null == undefined); // true
console.log(null === undefined); // false
// Особые случаи
console.log(NaN == NaN); // false
console.log(NaN === NaN); // false
console.log(Object.is(NaN, NaN)); // true
console.log(Object.is(0, -0)); // false
// Сравнение объектов (по ссылке)
let objA = { x: 1 };
let objB = { x: 1 };
console.log(objA == objB); // false (разные ссылки)Типичные ошибки:
- Проверка
value === NaNвсегда false; необходимоNumber.isNaN(value) - Сравнение двух одинаковых объектов с
===даёт false, если это разные ссылки - Использование
==может привести к неочевидным результатам:[] == 0-> true (через преобразование в строку и число)
Как копировать объекты (значения по ссылке)?
Поверхностное копирование: копируются только ссылки на вложенные объекты. Способы: Object.assign({}, source), оператор spread {...source}. Глубокое копирование: создаются новые копии всех вложенных структур. Простой вариант: JSON.parse(JSON.stringify(obj)) - не работает с функциями, undefined, Symbol, циклическими ссылками. Современное решение: structuredClone(obj) (поддержка в современных браузерах).
// Поверхностное копирование
let original = { a: 1, b: { c: 2 } };
let shallowCopy = { ...original };
shallowCopy.b.c = 100;
console.log(original.b.c); // 100 (изменение вложенного объекта) (скопирована ссылка)
// Глубокое копирование (JSON)
let deepCopy = JSON.parse(JSON.stringify(original));
deepCopy.b.c = 200;
console.log(original.b.c); // 100 (независимая копия)
// structuredClone (современный)
let clone = structuredClone(original);
clone.b.c = 300;
console.log(original.b.c); // 100Типичные ошибки:
- Поверхностное копирование оставляет внутренние объекты общими
JSON.parse(JSON.stringify(obj))теряетundefined, символы, функции, не поддерживает циклические ссылки- Неучёт того, что массивы - тоже объекты, и копирование spread
[...arr]поверхностное
Расширенные примеры работы со значениями
Упаковка примитивов (boxing)
При обращении к методу или свойству примитива (кроме null и undefined) JavaScript временно оборачивает его в объект-обёртку (String, Number, Boolean), после чего обёртка уничтожается. Это позволяет, например, вызывать 'text'.toUpperCase(), но не сохранять свойства на примитиве.
let primitiveString = 'Hello';
primitiveString.custom = 'world';
console.log(primitiveString.custom); // undefined (временная обёртка)
let wrapped = new String('Hello');
wrapped.custom = 'world';
console.log(wrapped.custom); // 'world' (объект сохраняет свойство)undefined world
Передача по ссылке: изменение внутри функции
function changeObj(obj) {
obj.name = 'changed';
}
let myObj = { name: 'original' };
changeObj(myObj);
console.log(myObj.name); // 'changed'
function replaceObj(obj) {
obj = { name: 'newObject' }; // параметр заменён на новую ссылку
}
let anotherObj = { name: 'old' };
replaceObj(anotherObj);
console.log(anotherObj.name); // 'old' (исходный объект не изменился)changed old
Значения BigInt и Symbol
let big = 1234567890123456789012345678901234567890n;
console.log(typeof big); // 'bigint'
let sym = Symbol('id');
let sym2 = Symbol('id');
console.log(sym == sym2); // false (каждый Symbol уникален)
// Типичная ошибка: BigInt нельзя смешивать с number в арифметике
// console.log(1n + 1); // TypeErrorbigint false
Проверка на NaN: isNaN vs Number.isNaN
console.log(isNaN('текст')); // true ('текст' приводится к NaN)
console.log(Number.isNaN('текст')); // false (не выполняет преобразование)
console.log(Number.isNaN(NaN)); // true
// Ещё одна особенность: NaN не равен самому себе
console.log(NaN === NaN); // falsetrue false true false
Преобразование в булевый тип (falsy значения)
// falsy: 0, '', null, undefined, NaN, false
function checkTruthy(value) {
if (value) {
console.log('truthy');
} else {
console.log('falsy');
}
}
checkTruthy(0); // falsy
checkTruthy(' '); // truthy (непустая строка)
checkTruthy([]); // truthy (пустой массив - объект, а объект truthy)
checkTruthy({}); // truthy
checkTruthy(NaN); // falsyfalsy truthy truthy truthy falsy
Глубокое сравнение объектов (рекурсивная функция)
function deepEqual(a, b) {
if (a === b) return true;
if (a == null || typeof a != 'object' || b == null || typeof b != 'object') return false;
const keysA = Object.keys(a), keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
for (let key of keysA) {
if (!keysB.includes(key)) return false;
if (!deepEqual(a[key], b[key])) return false;
}
return true;
}
const obj1 = { a: { b: 1 } };
const obj2 = { a: { b: 1 } };
const obj3 = { a: { b: 2 } };
console.log(deepEqual(obj1, obj2)); // true
console.log(deepEqual(obj1, obj3)); // falsetrue false
Пример с structuredClone и циклическими ссылками
let node = { name: 'root' };
node.self = node; // циклическая ссылка
// JSON.stringify выбросит ошибку
// console.log(JSON.parse(JSON.stringify(node))); // TypeError
// structuredClone справляется
let cloned = structuredClone(node);
console.log(cloned.name); // 'root'
console.log(cloned.self === cloned); // true (циклическая ссылка сохранена)root true
Неявное преобразование при сравнении с == (таблица)
console.log('' == 0); // true
console.log(' ' == 0); // true (пробел приводится к 0)
console.log('\t' == 0); // true
console.log([] == 0); // true (пустой массив -> '' -> 0)
console.log([1] == 1); // true ([1] -> '1' -> 1)
console.log([1,2] == '1,2'); // true
console.log(null == 0); // false (null преобразуется только к undefined)
console.log(undefined == null); // truetrue true true true true true false true