Перейти к основному содержимому

Понимание спецификации ECMAScript, часть 1

· 8 мин. чтения
[Марья Хёльтта](https://twitter.com/marjakh), наблюдатель за спецификациями

Все эпизоды

В этой статье мы рассмотрим простую функцию в спецификации и попробуем понять её обозначения. Поехали!

Введение

Даже если вы знаете JavaScript, читать его языковую спецификацию, спецификацию языка ECMAScript, или просто спецификацию ECMAScript, может быть довольно сложно. По крайней мере, я так чувствовал, когда впервые начал её читать.

Начнём с конкретного примера и разберём спецификацию, чтобы её понять. Следующий код демонстрирует использование Object.prototype.hasOwnProperty:

const o = { foo: 1 };
o.hasOwnProperty('foo'); // true
o.hasOwnProperty('bar'); // false

В этом примере объект o не имеет свойства под названием hasOwnProperty, поэтому мы поднимаемся по цепочке прототипов и ищем его. Мы находим его в прототипе объекта o, который является Object.prototype.

Чтобы описать, как работает Object.prototype.hasOwnProperty, спецификация использует описания, похожие на псевдокод:

Object.prototype.hasOwnProperty(V)

Когда вызывается метод hasOwnProperty с аргументом V, выполняются следующие шаги:

  1. Пусть P будет ? ToPropertyKey(V).
  2. Пусть O будет ? ToObject(this value).
  3. Вернуть ? HasOwnProperty(O, P).

…и…

HasOwnProperty(O, P)

Абстрактная операция HasOwnProperty используется для определения, имеет ли объект собственное свойство с указанным ключом свойства. Возвращается логическое значение. Операция вызывается с аргументами O и P, где O — это объект, а P — это ключ свойства. Эта абстрактная операция выполняет следующие шаги:

  1. Утвердить: Type(O) равен Object.
  2. Утвердить: IsPropertyKey(P) равен true.
  3. Пусть desc будет ? O.[[GetOwnProperty]](P).
  4. Если desc равен undefined, вернуть false.
  5. Вернуть true.

Но что такое «абстрактная операция»? Что находятся внутри [[ ]]? Почему перед функцией стоит ?? Что значат утверждения?

Давайте разберемся!

Языковые типы и типы спецификации

Начнем с чего-то знакомого. Спецификация использует значения, такие как undefined, true и false, которые мы уже знаем из JavaScript. Это все языковые значения, значения языковых типов, которые также определяет спецификация.

Спецификация также использует языковые значения внутри себя. Например, внутренний тип данных может содержать поле, возможные значения которого — true и false. В отличие от этого, движки JavaScript обычно не используют языковые значения внутри себя. Например, если движок JavaScript написан на C++, он, скорее всего, будет использовать C++-значения true и false (а не свои внутренние представления JavaScript-значений true и false).

Помимо языковых типов, спецификация также использует типы спецификации, которые существуют только в спецификации, но не в языке JavaScript. Движок JavaScript может (но не обязан) реализовывать их. В этом посте мы познакомимся с типом спецификации Record (и его подтипом Completion Record).

Абстрактные операции

Абстрактные операции — функции, определённые в спецификации ECMAScript; они предназначены для того, чтобы сделать описание спецификации более кратким. Движок JavaScript не обязан реализовывать их как отдельные функции внутри себя. Они не могут быть вызваны напрямую из JavaScript.

Внутренние слоты и внутренние методы

Внутренние слоты и внутренние методы используют имена, заключённые в [[ ]].

Внутренние слоты — это данные, хранящиеся в объекте JavaScript или в типе спецификации. Они используются для хранения состояния объекта. Внутренние методы — это функции, принадлежащие объекту JavaScript.

Например, у каждого объекта JavaScript есть внутренний слот [[Prototype]] и внутренний метод [[GetOwnProperty]].

Внутренние слоты и методы недоступны из JavaScript. Например, вы не можете получить доступ к o.[[Prototype]] или вызвать o.[[GetOwnProperty]](). Движок JavaScript может реализовать их для своего внутреннего использования, но не обязан.

Иногда внутренние методы делегируют выполнение абстрактным операциям с похожими названиями, как в случае обычных объектов и их [[GetOwnProperty]]:

[[GetOwnProperty]](P)

Когда внутренний метод [[GetOwnProperty]] объекта O вызывается с ключом свойства P, выполняются следующие шаги:

  1. Вернуть ! OrdinaryGetOwnProperty(O, P).

(В следующей главе мы выясним, что означает восклицательный знак.)

OrdinaryGetOwnProperty не является внутренним методом, так как он не связан ни с одним объектом; объект, с которым он работает, передается в качестве параметра.

OrdinaryGetOwnProperty называется “обычным”, поскольку он работает с обычными объектами. Объекты ECMAScript могут быть либо обычными, либо экзотическими. Обычные объекты должны иметь поведение по умолчанию для набора методов, называемых основными внутренними методами. Если объект отклоняется от поведения по умолчанию, он является экзотическим.

Самым известным экзотическим объектом является Array, так как его свойство длины (length) ведет себя необычным образом: установка свойства length может удалять элементы из массива.

Основные внутренние методы перечислены здесь.

Записи завершения

Что насчет вопросительных и восклицательных знаков? Чтобы понять их, нужно рассмотреть записи завершения!

Запись завершения — это тип спецификации (определен только для целей спецификации). Движок JavaScript не обязан иметь соответствующий внутренний тип данных.

Запись завершения — это “запись” — тип данных с фиксированным набором именованных полей. Запись завершения содержит три поля:

ПолеОписание
[[Type]]Одно из: normal (обычный), break (прерыв), continue (продолж.), return (возврат) или throw (ошибка). Все типы, кроме normal, являются резкими завершениями.
[[Value]]Значение, которое было получено при завершении, например возвращаемое значение функции или исключение (если оно было вызвано).
[[Target]]Используется для направленных передач управления (это не имеет значения для этого поста).

Каждая абстрактная операция неявно возвращает запись завершения. Даже если кажется, что абстрактная операция возвращает простой тип, такой как Boolean, он неявно оборачивается в запись завершения типа normal (см. Неявные значения завершения).

Примечание 1: Спецификация в этом отношении не полностью последовательна; есть вспомогательные функции, которые возвращают голые значения, и их возвращаемые значения используются как есть, без извлечения значения из записи завершения. Обычно это ясно из контекста.

Примечание 2: Редакторы спецификации изучают возможность более явного управления записями завершения.

Если алгоритм выбрасывает исключение, это означает возврат записи завершения с [[Type]] throw, где [[Value]] является объектом исключения. Мы пока проигнорируем типы break, continue и return.

ReturnIfAbrupt(argument) означает выполнение следующих шагов:

  1. Если argument является резким завершением, вернуть argument.
  2. Присвоить argument значение argument.[[Value]].

То есть мы проверяем запись завершения; если это резкое завершение, мы сразу возвращаем ее. В противном случае извлекаем значение из записи завершения.

ReturnIfAbrupt может выглядеть как вызов функции, но это не так. Это приводит к тому, что функция, в которой появляется ReturnIfAbrupt(), возвращает значение, а не сама функция ReturnIfAbrupt. Это ведет себя больше как макрос в языках типа C.

ReturnIfAbrupt можно использовать следующим образом:

  1. Пусть obj будет результатом Foo(). (obj — это запись завершения.)
  2. ReturnIfAbrupt(obj).
  3. Bar(obj). (Если мы все еще здесь, то obj — значение, извлеченное из записи завершения.)

И теперь вопросительный знак: ? Foo() эквивалентен ReturnIfAbrupt(Foo()). Использование сокращения удобно: нам не нужно явно писать код обработки ошибок каждый раз.

Аналогично, Let val be ! Foo() эквивалентно следующему:

  1. Пусть val будет результатом вызова Foo().
  2. Утверждение: val не является резким завершением.
  3. Присвоить val значение val.[[Value]].

Используя эти знания, мы можем переписать Object.prototype.hasOwnProperty следующим образом:

Object.prototype.hasOwnProperty(V)

  1. Пусть P будет ToPropertyKey(V).
  2. Если P является прерыванием выполнения, вернуть P.
  3. Установить P в P.[[Value]].
  4. Пусть O будет ToObject(this value).
  5. Если O является прерыванием выполнения, вернуть O.
  6. Установить O в O.[[Value]].
  7. Пусть temp будет HasOwnProperty(O, P).
  8. Если temp является прерыванием выполнения, вернуть temp.
  9. Установить temp в temp.[[Value]].
  10. Вернуть NormalCompletion(temp).

…и мы можем переписать HasOwnProperty следующим образом:

HasOwnProperty(O, P)

  1. Утверждение: Type(O) — это Object.
  2. Утверждение: IsPropertyKey(P) — это true.
  3. Пусть desc будет O.[[GetOwnProperty]](P).
  4. Если desc является прерыванием выполнения, вернуть desc.
  5. Установить desc в desc.[[Value]].
  6. Если desc — это undefined, вернуть NormalCompletion(false).
  7. Вернуть NormalCompletion(true).

Мы также можем переписать внутренний метод [[GetOwnProperty]] без восклицательного знака:

O.[[GetOwnProperty]]

  1. Пусть temp будет OrdinaryGetOwnProperty(O, P).
  2. Утверждение: temp не является прерыванием выполнения.
  3. Установить temp в temp.[[Value]].
  4. Вернуть NormalCompletion(temp).

Здесь мы предполагаем, что temp — это совершенно новая временная переменная, которая ни с чем больше не пересекается.

Мы также использовали знание о том, что когда инструкция return возвращает что-то отличное от записи завершения (Completion Record), это неявно оборачивается в NormalCompletion.

Отступление: Return ? Foo()

В спецификации используется нотация Return ? Foo() — зачем нужен вопросительный знак?

Return ? Foo() раскрывается в следующем:

  1. Пусть temp будет Foo().
  2. Если temp является прерыванием выполнения, вернуть temp.
  3. Установить temp в temp.[[Value]].
  4. Вернуть NormalCompletion(temp).

Что эквивалентно Return Foo(); это ведет себя одинаково как для прерывистых, так и для нормальных завершений.

Return ? Foo() используется только для редакторских целей, чтобы сделать явно, что Foo возвращает запись завершения (Completion Record).

Утверждения

Утверждения в спецификации подтверждают инвариантные условия алгоритмов. Они добавлены для ясности, но не добавляют никаких требований к реализации — реализация не обязана их проверять.

Переход к следующему

Абстрактные операции делегируют выполнение другим абстрактным операциям (см. рисунок ниже), но на основе этого поста мы должны быть в состоянии определить, что они делают. Мы столкнемся с дескрипторами свойств, которые являются еще одним типом спецификаций.

Граф вызовов функций, начинающийся с Object.prototype.hasOwnProperty

Резюме

Мы прочитали простой метод — Object.prototype.hasOwnProperty — и абстрактные операции, которые он вызывает. Мы познакомились с сокращениями ? и !, связанными с обработкой ошибок. Мы изучили языковые типы, типы спецификаций, внутренние слоты и внутренние методы.

Полезные ссылки

Как читать спецификацию ECMAScript: руководство, которое охватывает большинство материала, описанного в этом посте, но с немного другого угла зрения.