Все о значениях в языке JavaScript: от типов до глубокого копирования

Раздел: 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); // TypeError
bigint
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); // false
true
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);         // falsy
falsy
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)); // false
true
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); // true
true
true
true
true
true
true
false
true

Значения в JavaScript - comments

En
значения js (javascript)