Урок 09

Копирование и сравнение

Примитивы

Копируются по значению, сравниваются по значению:

let a = 5;
let b = a;
b = 10;
a; // 5

5 === 5; // true

Объекты и массивы

Копируются по ссылке:

const a = { x: 1 };
const b = a;
b.x = 2;
a.x; // 2 — тот же объект

Сравниваются по ссылке:

{} === {};   // false
[] === [];   // false

const a = {};
const b = a;
a === b;     // true

Поверхностная копия

Массив

const copy = [...arr];
const copy2 = arr.slice();
const copy3 = Array.from(arr);

Объект

const copy = { ...obj };
const copy2 = Object.assign({}, obj);

Проблема с вложенными:

const original = { user: { name: 'Алиса' } };
const copy = { ...original };

copy.user.name = 'Боб';
original.user.name; // 'Боб' — вложенный тот же

Глубокая копия

structuredClone

Современный способ:

const deep = structuredClone(obj);

Работает с Date, Map, Set, ArrayBuffer. Не работает с функциями и DOM.

JSON

Быстро и грязно:

const deep = JSON.parse(JSON.stringify(obj));

Проблемы:

  • Теряет функции
  • Теряет undefined
  • Date становится строкой
  • Не работает с циклами
  • Не работает с Map, Set

Ручная рекурсия

function deepClone(value) {
    if (value === null || typeof value !== 'object') return value;
    if (value instanceof Date) return new Date(value);
    if (Array.isArray(value)) return value.map(deepClone);
    if (value instanceof Map) return new Map([...value].map(([k, v]) => [deepClone(k), deepClone(v)]));
    if (value instanceof Set) return new Set([...value].map(deepClone));

    const result = {};
    for (const [key, v] of Object.entries(value)) {
        result[key] = deepClone(v);
    }
    return result;
}

Сравнение

Примитивы

1 === 1;         // true
'а' === 'а';     // true
true === true;   // true
NaN === NaN;     // false

Объекты

По ссылке:

const a = { x: 1 };
const b = { x: 1 };
a === b; // false
a === a; // true

Структурное сравнение

Простые случаи:

function shallowEqual(a, b) {
    const ka = Object.keys(a);
    const kb = Object.keys(b);
    if (ka.length !== kb.length) return false;
    return ka.every((k) => a[k] === b[k]);
}

Глубокое:

function deepEqual(a, b) {
    if (a === b) return true;
    if (typeof a !== typeof b) return false;
    if (a === null || b === null) return false;
    if (typeof a !== 'object') return false;

    const ka = Object.keys(a);
    const kb = Object.keys(b);

    if (ka.length !== kb.length) return false;

    return ka.every((k) => deepEqual(a[k], b[k]));
}

Готовые библиотеки

  • lodash — isEqual, cloneDeep
  • fast-deep-equal — быстрая проверка
  • rfdc — быстрый клон
import { isEqual, cloneDeep } from 'lodash';

isEqual({ a: 1 }, { a: 1 }); // true
cloneDeep(obj);

Проблема с React и другими

React использует === для проверки изменений. Мутация объекта не вызывает ререндер:

const [user, setUser] = useState({ name: 'Алиса' });

user.name = 'Боб';       // не сработает
setUser({ ...user, name: 'Боб' }); // ок

Всегда создавай новые объекты.

Иммутабельные обновления

// Массив — добавить
setItems((prev) => [...prev, item]);

// Массив — удалить
setItems((prev) => prev.filter((i) => i.id !== id));

// Объект — обновить
setUser((prev) => ({ ...prev, name: 'Боб' }));

// Вложенный объект
setUser((prev) => ({
    ...prev,
    address: { ...prev.address, city: 'Питер' },
}));

Итоги

  • Примитивы — по значению
  • Объекты — по ссылке
  • spread — поверхностная копия
  • structuredClone — глубокая
  • JSON — грязный способ
  • lodash.isEqual для глубокого сравнения
  • Мутация не сработает в React