Понимание спецификации 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: руководство, которое охватывает большинство материала, описанного в этом посте, но с немного другого угла зрения.