Раздел 3 · JavaScript

Объекты, массивы, прототипы

junior~55 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Объекты, массивы, прототипы

В JavaScript почти всё, кроме семи примитивов, это объект. Массив, функция, дата, регулярка, Map, Set, Promise, узлы DOM, даже сам globalThis. Снаружи они выглядят разными конструкциями со своими литералами и методами, но внутри устроены одинаково: набор свойств (bag of properties), прицепленный к прототипу, у которого тоже есть прототип, и так до Object.prototype или null в самом конце цепочки. Понять эту модель один раз, и большинство сюрпризов языка перестают быть сюрпризами.

В предыдущем уроке мы разобрались с функциями. Теперь пора посмотреть на сами значения, которые функции принимают и возвращают. Через объекты язык реализует наследование, инкапсуляцию, итерацию, замораживание данных, кастомное приведение к примитиву. Массивы дают упорядоченные коллекции и почти всё, что нужно для алгоритмов на JS. Классы это синтаксический сахар над прототипами, без которого язык прекрасно работал бы и так, но с которым код становится привычнее тем, кто пришёл с Java или C#.

Этот урок объединяет три темы, которые в курсе Шемседдинова шли отдельными неделями: объекты и property descriptors, массивы и их методы (включая иммутабельные двойники из ES2023), прототипы и классы. И отдельно поговорим про композицию против наследования, выбор, который определяет, как ты будешь писать код следующие пять лет.

1. Объект как property bag

Базовое создание:

// объектный литерал
const user = { id: 1, name: 'Bat', role: 'admin' };

// доступ
user.name;          // 'Bat', точечный
user['name'];       // 'Bat', индексный, имя через []
user[`na${'me'}`];  // 'Bat', динамический ключ

// computed property name на создании
const key = 'role';
const u = { id: 1, [key]: 'admin' };  // { id: 1, role: 'admin' }

// shorthand: name === user.name по значению переменной
const id = 1;
const name = 'Bat';
const u2 = { id, name };  // { id: 1, name: 'Bat' }

// spread: shallow copy + override
const updated = { ...user, role: 'guest' };

Ключи объекта, всегда строки или Symbol. Числовой ключ автоматически приводится к строке ({ 1: 'one' }, ключ '1'). Symbol-ключ нужен для well-known protocols (Symbol.iterator, Symbol.toPrimitive), подробнее в уроке 08.

1.1. Property descriptors

У каждого свойства, кроме значения, ещё три флага. Это и есть property descriptor:

const o = {};
Object.defineProperty(o, 'name', {
  value: 'Bat',
  writable: false,    // нельзя присвоить новое значение
  enumerable: false,  // не виден в for...in, Object.keys, JSON.stringify
  configurable: false, // нельзя удалить или переопределить дескриптор
});

Object.getOwnPropertyDescriptor(o, 'name');
// { value: 'Bat', writable: false, enumerable: false, configurable: false }

Литералы создают свойства со всеми флагами true. defineProperty, способ заявить особый контракт: read-only, скрытое, с getter и setter.

const account = {};
Object.defineProperty(account, 'balance', {
  get() { return this._kop / 100; },
  set(value) { this._kop = Math.round(value * 100); },
  enumerable: true,
  configurable: true,
});

account.balance = 12.5;
account.balance;     // 12.5
account._kop;        // 1250

В новом коде getter/setter чаще всего лишние: явная функция читается понятнее. Используй их, когда оборачиваешь legacy-объект, делаешь Proxy-наблюдатель или собираешь fluent API.

1.2. Object methods

МетодЧто делает
Object.keys(o)массив enumerable string-ключей
Object.values(o)массив значений
Object.entries(o)массив [key, value] пар
Object.fromEntries(arr)обратное к entries: из пар собирает объект
Object.assign(target, ...src)shallow merge с мутацией target
Object.create(proto, props?)создать объект с заданным прототипом
Object.freeze(o)заморозить (shallow)
Object.seal(o)запретить добавлять/удалять, но менять можно
Object.preventExtensions(o)запретить только добавлять новые
Object.is(a, b)как ===, но с правильным NaN и -0
Object.hasOwn(o, key)hasOwnProperty без прототипных сюрпризов
Object.groupBy(arr, fn)сгруппировать массив по ключу из fn (ES2024)
structuredClone(o)глубокая копия с поддержкой циклов и Map/Set/Date

Object.entries + map + Object.fromEntries, идиоматичная трансформация словаря:

const prices = { apple: 100, pear: 80 };
const discounted = Object.fromEntries(
  Object.entries(prices).map(([k, v]) => [k, v * 0.9])
);
// { apple: 90, pear: 72 }

Object.groupBy (ES2024):

const items = [
  { city: 'Vilnius', t: -2 },
  { city: 'Vilnius', t: -1 },
  { city: 'Riga', t: 1 },
];

Object.groupBy(items, (x) => x.city);
// { Vilnius: [{...}, {...}], Riga: [{...}] }

В Node 22+ доступно нативно. До того, рукой через reduce:

const groupBy = (xs, key) =>
  xs.reduce((acc, x) => {
    const k = key(x);
    (acc[k] ??= []).push(x);
    return acc;
  }, {});

1.3. Передача по ссылке и мутация

Объекты передаются по ссылке. Мутация внутри функции видна снаружи:

function applyDefaults(opts) {
  opts.timeout = opts.timeout ?? 5000;  // мутация: внешний opts тоже изменён
  return opts;
}

const original = { method: 'GET' };
applyDefaults(original);
original.timeout;  // 5000, мы изменили внешний объект

Защита, копирование. По возможности shallow:

function applyDefaults(opts) {
  return { timeout: 5000, ...opts };  // новый объект
}

Когда нужен deep copy:

СпособКогда
spread { ...obj }shallow, объект простой
structuredClone(obj)deep, поддержка Map/Set/Date/циклов (Node 17+)
JSON.parse(JSON.stringify(obj))deep, теряет Date/Map/Set/undefined/функции
immer / immutable.jsкопирование в стиле persistent data structures

structuredClone, по умолчанию правильный выбор. Быстрее JSON-парсинга, не теряет типы, поддерживает циклы.

1.4. Заморозка

Три уровня заморозки по возрастанию строгости: preventExtensions (нельзя добавлять новые), seal (плюс нельзя удалять), freeze (плюс нельзя менять).

const frozen = Object.freeze({ x: 1, nested: { y: 2 } });
frozen.x = 2;            // в strict TypeError, в sloppy тихий no-op
frozen.x;                // 1
frozen.nested.y = 99;    // прошло! freeze shallow
frozen.nested.y;         // 99

Глубокая заморозка через рекурсию:

const deepFreeze = (o) => {
  for (const v of Object.values(o)) {
    if (v && typeof v === 'object') deepFreeze(v);
  }
  return Object.freeze(o);
};

В реальном коде заморозку используют для конфигов, init-payload и тестовых фикстур, всё, что не должно меняться после загрузки. В горячем пути Object.freeze стоит дороже, чем кажется (V8 ставит объект в “frozen” hidden class), не злоупотребляй на каждом DTO.

Версия выше честно работает на дереве конфига, но споткнётся о цикл, не увидит ключей-символов и заморозит Map для вида. Доведи её до ума: тесты бегут прямо в браузере и пробуют писать в объект на любой глубине.

дальше после входа

Урок целиком открыт всем, кто вошёл. Войди по своему email, чтобы читать дальше.

Вход пока по приглашению. Если хочешь учиться, напиши Vassiliy.