Понимание спецификации ECMAScript, часть 1
В этой статье мы рассмотрим простую функцию в спецификации и попробуем понять её обозначения. Поехали!
Введение
Даже если вы знаете 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
, выполняются следующие шаги:
- Пусть
P
будет? ToPropertyKey(V)
.- Пусть
O
будет? ToObject(this value)
.- Вернуть
? HasOwnProperty(O, P)
.
…и…
Абстрактная операция
HasOwnProperty
используется для определения, имеет ли объект собственное свойство с указанным ключом свойства. Возвращается логическое значение. Операция вызывается с аргументамиO
иP
, гдеO
— это объект, аP
— это ключ свойства. Эта абстрактная операция выполняет следующие шаги:
- Утвердить:
Type(O)
равенObject
.- Утвердить:
IsPropertyKey(P)
равенtrue
.- Пусть
desc
будет? O.[[GetOwnProperty]](P)
.- Если
desc
равенundefined
, вернутьfalse
.- Вернуть
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]]
объектаO
вызывается с ключом свойстваP
, выполняются следующие шаги:
- Вернуть
! 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)
означает выполнение следующих шагов:
- Если
argument
является резким завершением, вернутьargument
.- Присвоить
argument
значениеargument.[[Value]]
.
То есть мы проверяем запись завершения; если это резкое завершение, мы сразу возвращаем ее. В противном случае извлекаем значение из записи завершения.
ReturnIfAbrupt
может выглядеть как вызов функции, но это не так. Это приводит к тому, что функция, в которой появляется ReturnIfAbrupt()
, возвращает значение, а не сама функция ReturnIfAbrupt
. Это ведет себя больше как макрос в языках типа C.
ReturnIfAbrupt
можно использовать следующим образом:
- Пусть
obj
будет результатомFoo()
. (obj
— это запись завершения.)ReturnIfAbrupt(obj)
.Bar(obj)
. (Если мы все еще здесь, тоobj
— значение, извлеченное из записи завершения.)
И теперь вопросительный знак: ? Foo()
эквивалентен ReturnIfAbrupt(Foo())
. Использование сокращения удобно: нам не нужно явно писать код обработки ошибок каждый раз.
Аналогично, Let val be ! Foo()
эквивалентно следующему:
- Пусть
val
будет результатом вызоваFoo()
.- Утверждение:
val
не является резким завершением.- Присвоить
val
значениеval.[[Value]]
.
Используя эти знания, мы можем переписать Object.prototype.hasOwnProperty
следующим образом:
Object.prototype.hasOwnProperty(V)
- Пусть
P
будетToPropertyKey(V)
.- Если
P
является прерыванием выполнения, вернутьP
.- Установить
P
вP.[[Value]]
.- Пусть
O
будетToObject(this value)
.- Если
O
является прерыванием выполнения, вернутьO
.- Установить
O
вO.[[Value]]
.- Пусть
temp
будетHasOwnProperty(O, P)
.- Если
temp
является прерыванием выполнения, вернутьtemp
.- Установить
temp
вtemp.[[Value]]
.- Вернуть
NormalCompletion(temp)
.
…и мы можем переписать HasOwnProperty
следующим образом:
HasOwnProperty(O, P)
- Утверждение:
Type(O)
— этоObject
.- Утверждение:
IsPropertyKey(P)
— этоtrue
.- Пусть
desc
будетO.[[GetOwnProperty]](P)
.- Если
desc
является прерыванием выполнения, вернутьdesc
.- Установить
desc
вdesc.[[Value]]
.- Если
desc
— этоundefined
, вернутьNormalCompletion(false)
.- Вернуть
NormalCompletion(true)
.
Мы также можем переписать внутренний метод [[GetOwnProperty]]
без восклицательного знака:
O.[[GetOwnProperty]]
- Пусть
temp
будетOrdinaryGetOwnProperty(O, P)
.- Утверждение:
temp
не является прерыванием выполнения.- Установить
temp
вtemp.[[Value]]
.- Вернуть
NormalCompletion(temp)
.
Здесь мы предполагаем, что temp
— это совершенно новая временная переменная, которая ни с чем больше не пересекается.
Мы также использовали знание о том, что когда инструкция return
возвращает что-то отличное от записи завершения (Completion Record), это неявно оборачивается в NormalCompletion
.
Отступление: Return ? Foo()
В спецификации используется нотация Return ? Foo()
— зачем нужен вопросительный знак?
Return ? Foo()
раскрывается в следующем:
- Пусть
temp
будетFoo()
.- Если
temp
является прерыванием выполнения, вернутьtemp
.- Установить
temp
вtemp.[[Value]]
.- Вернуть
NormalCompletion(temp)
.
Что эквивалентно Return Foo()
; это ведет себя одинаково как для прерывистых, так и для нормальных завершений.
Return ? Foo()
используется только для редакторских целей, чтобы сделать явно, что Foo
возвращает запись завершения (Completion Record).
Утверждения
Утверждения в спецификации подтверждают инвариантные условия алгоритмов. Они добавлены для ясности, но не добавляют никаких требований к реализации — реализация не обязана их проверять.
Переход к следующему
Абстрактные операции делегируют выполнение другим абстрактным операциям (см. рисунок ниже), но на основе этого поста мы должны быть в состоянии определить, что они делают. Мы столкнемся с дескрипторами свойств, которые являются еще одним типом спецификаций.
Резюме
Мы прочитали простой метод — Object.prototype.hasOwnProperty
— и абстрактные операции, которые он вызывает. Мы познакомились с сокращениями ?
и !
, связанными с обработкой ошибок. Мы изучили языковые типы, типы спецификаций, внутренние слоты и внутренние методы.
Полезные ссылки
Как читать спецификацию ECMAScript: руководство, которое охватывает большинство материала, описанного в этом посте, но с немного другого угла зрения.