Внутренние методы Object (спецификация ECMAScript)

Согласно спецификации, объект в JavaScript — не «коробка с ключами и значениями», а набор из 11 internal methods (+2 для callable). Привычное представление об объектах — это аналогия, не реальность.

«Согласно спецификации, объект — это некоторая коробка, в которой есть 13 методов и больше ничего нет. Нет в нём properties, ничего другого. Самое главное для определения семантики объекта — эти 13 методов» — автор, 6FqwosOqJCs.

11 базовых internal methods

В нотации спецификации записываются в двойных квадратных скобках [[Method]]:

Метод Что делает
[[GetPrototypeOf]] возвращает прототип объекта
[[SetPrototypeOf]] устанавливает прототип
[[IsExtensible]] можно ли добавлять свойства
[[PreventExtensions]] запрещает добавление свойств
[[GetOwnProperty]] возвращает дескриптор собственного свойства
[[DefineOwnProperty]] определяет собственное свойство
[[HasProperty]] проверка наличия (включая прототип)
[[Get]] чтение свойства
[[Set]] запись свойства
[[Delete]] удаление свойства
[[OwnPropertyKeys]] список собственных ключей

+2 для callable объектов

Метод Что делает
[[Call]] вызывает функцию
[[Construct]] вызывает как конструктор (new)

Если у объекта есть [[Call]] — это callable (функция). Если ещё и [[Construct]] — может вызываться с new.

Ordinary vs Exotic Objects

Ordinary object — все 11 (или 13) методов следуют стандартным алгоритмам из спецификации. Это обычный {}.

Exotic object — хотя бы один метод переопределён собственным алгоритмом:

  • Proxy — все методы перехватываются handler-ами
  • Array[[DefineOwnProperty]] поддерживает поведение length
  • Function — особое [[Call]]/[[Construct]]
  • Arguments, String-обёртки, TypedArray, Module Namespace — у всех свои отклонения

Object literal — не объект, а команда

const o = { name: 'Marcus' };

«Object literal — это синтаксическая команда, которая заставляет внутреннюю машинерию определённым образом создать объект. Это не объект, а команда из нескольких подкоманд» — автор.

Литерал → команда OrdinaryObjectCreate(%Object.prototype%) + последовательность [[DefineOwnProperty]] для каждого поля.

Ключи: только строки и символы

«Числовых ключей в JavaScript не существует. Единственные ключи — строки и символы.»

const o = { 1: 'a' };
o[1] === o['1']; // true — '1' приведено к строке через ToPropertyKey

Алгоритм ToPropertyKey приводит любой не-Symbol к строке. Symbol — единственное исключение.

Property Descriptor

Каждое свойство в объекте описано property descriptor — внутренней структурой:

Object.getOwnPropertyDescriptor({ x: 1 }, 'x');
// { value: 1, writable: true, enumerable: true, configurable: true }
  • Data descriptor: value, writable
  • Accessor descriptor: get, set
  • Общие: enumerable, configurable

Reference Record и receiver

Когда мы пишем obj.method, под капотом создаётся Reference Record — структура с base (объект) и referenced name (имя). Это нужно, чтобы при вызове передать receiver (то, что станет this).

Поэтому obj.method и obj['method'] дают разный this, чем const m = obj.method; m — во втором случае reference уже разрешён, receiver потерян.

Прототип и [[Get]]

Алгоритм OrdinaryGet(obj, key, receiver):

  1. Получить дескриптор через [[GetOwnProperty]].
  2. Если дескриптора нет — рекурсивно вызвать [[Get]] у прототипа ([[GetPrototypeOf]]).
  3. Если data — вернуть value. Если accessor — вызвать get с receiver как this.

Это и есть прототипная цепочка на уровне спецификации.

Зачем это знать

  1. Понимать Proxy — handler перехватывает именно эти 13 методов, один в один по имени.
  2. Понимать V8 — engine компилирует эти алгоритмы в машинный код с inline-кэшами.
  3. Не путаться в граничных случаяхdelete, in, Object.keys(), for-in, Reflect.ownKeys все читают разные методы.

Источники

Связанные темы