Объекты, массивы, прототипы
В 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.