Объект в JavaScript - набор пар «ключ - значение». Почти всё в языке либо объект, либо ведёт себя как объект: массивы, функции, даты. Разберём, как объекты создавать, читать и изменять, как перебирать и проверять свойства, как правильно копировать и почему два одинаковых объекта не равны друг другу.

Создание

Чаще всего объект создают литералом:

js
const user = {
    name: 'Иван',
    age: 30,
    'favorite color': 'синий',
};

Ключи - строки (или символы). Кавычки нужны, только если ключ не является допустимым идентификатором, например содержит пробел или дефис.

Сокращённая запись свойств - если имя переменной совпадает с ключом:

js
const name = 'Иван';
const age = 30;
const user = { name, age };          // то же, что { name: name, age: age }

Вычисляемые ключи - имя свойства берётся из выражения:

js
const field = 'email';
const user = { [field]: 'ivan@example.com', [`${field}Verified`]: true };

Другие способы: Object.create(proto) создаёт объект с заданным прототипом (Object.create(null) - объект совсем без прототипа, удобен как словарь), Object.fromEntries(pairs) - объект из массива пар, а классы - об этом ниже.

Доступ к свойствам

js
user.name                 // точечная запись
user['favorite color']    // квадратные скобки: для любых ключей и ключей из переменных
user[field]

user.city = 'Бишкек';     // добавить или изменить
delete user.city;         // удалить

Обращение к несуществующему свойству возвращает undefined, а не ошибку. Ошибка возникает, когда пытаются прочитать свойство у undefined или null. От неё защищает опциональная цепочка:

js
const city = user.address?.city;            // undefined, если address нет
const first = user.orders?.[0];
user.notify?.();                            // вызвать, только если метод есть

Значение по умолчанию удобно задавать оператором ?? - он срабатывает только на null и undefined, в отличие от ||, который заменяет и 0, и пустую строку:

js
const limit = settings.limit ?? 10;

Методы и this

Функция в свойстве объекта - метод. Внутри метода this указывает на объект, у которого метод вызван:

js
const counter = {
    value: 0,
    increment() {
        this.value++;
        return this;
    },
};

counter.increment().increment();
counter.value;   // 2

this определяется в момент вызова, а не при создании. Если передать метод как отдельную функцию, связь потеряется:

js
const inc = counter.increment;
inc();                         // this не counter - ошибка или неверный результат
const bound = counter.increment.bind(counter);   // так связь сохраняется

У стрелочных функций своего this нет, они берут его из окружающего кода. Поэтому стрелочные функции не подходят для методов, но удобны внутри методов - например, в обработчиках и колбэках.

Геттеры и сеттеры

Свойство может вычисляться при чтении и проверять значение при записи:

js
const user = {
    firstName: 'Иван',
    lastName: 'Петров',

    get fullName() {
        return `${this.firstName} ${this.lastName}`;
    },

    set fullName(value) {
        [this.firstName, this.lastName] = value.split(' ');
    },
};

user.fullName;                    // 'Иван Петров'
user.fullName = 'Пётр Сидоров';
user.firstName;                   // 'Пётр'

Перебор свойств

js
Object.keys(user)       // ['firstName', 'lastName', ...] - собственные перечисляемые ключи
Object.values(user)     // значения
Object.entries(user)    // пары [ключ, значение]

for (const [key, value] of Object.entries(user)) {
    console.log(key, value);
}

Цикл for...in тоже перебирает ключи, но вместе с унаследованными перечисляемыми свойствами из прототипа. В обычном коде удобнее и безопаснее Object.keys и Object.entries.

Порядок ключей: сначала целочисленные ключи по возрастанию, затем строковые в порядке добавления.

Преобразовать объект, например умножить все цены:

js
const prices = { apple: 100, pear: 150 };
const doubled = Object.fromEntries(
    Object.entries(prices).map(([key, value]) => [key, value * 2])
);

Проверка свойств

js
'name' in user                  // true, если свойство есть (в том числе в прототипе)
Object.hasOwn(user, 'name')     // true, только если это собственное свойство
user.name !== undefined         // не надёжно: свойство может существовать со значением undefined

Object.hasOwn - современная замена user.hasOwnProperty('name'), которая работает и для объектов без прототипа.

Деструктуризация

js
const { name, age = 18, address: { city } = {} } = user;
  • age = 18 - значение по умолчанию, если свойства нет;
  • address: { city } - вложенная деструктуризация, = {} защищает от отсутствия address.

Переименование и остаток:

js
const { name: userName, ...rest } = user;    // rest - все остальные свойства

В параметрах функции деструктуризация заменяет длинные списки аргументов:

js
function createUser({ name, role = 'user', active = true }) { /* ... */ }
createUser({ name: 'Иван', role: 'admin' });

Объекты передаются по ссылке

Два ярлыка со стрелками к одному объекту, изменённое поле выделено

Переменная хранит не сам объект, а ссылку на него. Присваивание копирует ссылку:

js
const a = { x: 1 };
const b = a;
b.x = 2;
a.x;    // 2 - это один и тот же объект

Поэтому функция, изменившая полученный объект, меняет его и для вызывающего кода. И поэтому сравнение === проверяет, один ли это объект, а не одинаковое ли содержимое:

js
{ x: 1 } === { x: 1 }    // false

Сравнить содержимое простых объектов можно своей функцией или через JSON.stringify (только если порядок ключей совпадает и в объектах нет функций, дат и undefined).

const запрещает переназначить переменную, но не менять объект: const user = {}; user.name = 'Иван'; работает.

Копирование

Вверху две копии делят общий вложенный объект, внизу у каждой свой

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

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

Копируются свойства первого уровня. Вложенные объекты остаются общими:

js
const original = { name: 'Иван', address: { city: 'Бишкек' } };
const copy = { ...original };
copy.address.city = 'Ош';
original.address.city;    // 'Ош' - вложенный объект общий

Spread удобен и для «изменения» объекта без мутации - это основа работы с состоянием во многих фреймворках:

js
const updated = { ...user, age: 31 };

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

js
const deep = structuredClone(original);

structuredClone встроен в браузеры и Node.js и копирует вложенные объекты, массивы, даты, Map, Set и даже циклические ссылки. Не копирует функции и DOM-элементы, а у экземпляров классов теряется прототип - получится обычный объект.

Старый способ JSON.parse(JSON.stringify(obj)) до сих пор встречается, но теряет undefined, функции, превращает даты в строки и падает на циклических ссылках. В новом коде используйте structuredClone.

Защита от изменений

js
Object.freeze(config);       // нельзя добавлять, удалять и менять свойства
Object.seal(config);         // нельзя добавлять и удалять, менять можно
Object.isFrozen(config);

Заморозка поверхностная: вложенные объекты остаются изменяемыми, если не заморозить и их.

Тонкая настройка отдельного свойства - Object.defineProperty:

js
Object.defineProperty(user, 'id', {
    value: 42,
    writable: false,        // нельзя изменить
    enumerable: false,      // не видно в Object.keys и JSON
});

Прототипы и классы

У каждого объекта есть прототип - объект, у которого он «наследует» свойства и методы. Когда свойства нет в самом объекте, JavaScript ищет его в прототипе, потом в прототипе прототипа и так далее. Именно поэтому у любого массива есть map, а у любого объекта - toString.

Классы - удобный синтаксис для создания объектов с общими методами в прототипе:

js
class User {
    #password;                              // приватное поле

    constructor(name, password) {
        this.name = name;
        this.#password = password;
    }

    checkPassword(value) {
        return this.#password === value;
    }

    static fromJSON(json) {
        const data = JSON.parse(json);
        return new User(data.name, data.password);
    }
}

class Admin extends User {
    isAdmin = true;
}

const admin = new Admin('Иван', 'secret');
admin.checkPassword('secret');    // true
admin instanceof User;            // true

Поля с # недоступны снаружи класса: admin.#password - синтаксическая ошибка.

Объект или Map

Для словарей с часто меняющимися или произвольными ключами (идентификаторы, строки от пользователя) лучше подходит Map: ключи любого типа, свой метод size, понятный порядок и нет унаследованных свойств вроде toString, которые могут совпасть с ключом. Объект удобнее для структур с заранее известными полями и для обмена данными через JSON.