Доброго времени суток, друзья!
В данной статье мы возьмем функцию из спецификации и разберем ее объяснение. Поехали.
Предисловие
Даже если вы хорошо знаете JavaScript, чтение спецификации может быть затруднительным. Следующий код демонстрирует использование Object.prototype.hasOwnProperty:
const o = {
foo: 1
}
o.hasOwnProperty('foo') // true
o.hasOwnProperty('bar') // false
В примере объект «o» не имеет метода «hasOwnProperty», поэтому мы обращаемся к его прототипу — «Object.prototype» (цепочка прототипов).
Для описания того, как работает Object.hasOwnProperty, в спецификации используется следующий псевдокод:
Object.prototype.hasOwnProperty(V)
Когда hasOwnProperty вызывается с аргументом V, выполняются следующие шаги:
- Пусть P будет ? ToPropertyKey(V)
- Пусть O будет ? ToObject(значение this)
- Вернуть ? HasOwnProperty.
… и
Абстрактная операция HasOwnProperty используется для установления того, имеет ли объект собственное свойство с определенным ключом. Возвращается логическое значение. Операция вызывается с аргументами O и P. Эта операция состоит из следующих этапов:
- Утверждение (assert): Type(O) является Object.
- Утверждение: IsPropertyKey(P) является true.
- Пусть desc будет ? O.[[GetOwnProperty]](P).
- Если desc является undefined, вернуть false.
- Вернуть true.
Что такое «абстрактная операция»? Что такое [[ ]]? Почему перед функцией стоит вопросительный знак? Что означает «утверждение»?
Давайте это выясним.
Типы в языке и типы в спецификации
Начнем с чего-то знакомого. В спецификации встречаются такие значения, как undefined, true и false, которые известны нам по JS. Все они являются «языковыми значениями» (language values), «значениями языковых типов» (values of language types), которые также определяются спецификацией.
Спецификация использует встроенные языковые значения, например, внутренний тип данных может содержать поле со значением true или false. «Движки» JS обычно не используют языковые значения в своих внутренних механизмах. Например, если движок JS написан на C++, он скорее всего будет использовать true и false из C++, а не внутреннее представление булевых значений из JS.
В дополнение к языковым типам, в спецификации используются специальные типы (specification types), представляющие собой типы, которые используются только в спецификации, но не в языке JS. Движки JS не обязаны их выполнять (но могут). В данной статье мы познакомимся со специальным типом «Record» (запись) и его подтипом «Completion Record» (запись о завершении).
Абстрактные операции
Абстрактные операции — это функции, определенные в спецификации; они определяются в целях сокращения спецификации. Движки JS не обязаны выполнять их в качестве самостоятельных функций. В JS их нельзя вызывать напрямую.
Внутренние слоты и внутренние методы
Внутренние слоты и внутренние методы обозначаются именами, заключенными в [[ ]].
Внутренние слоты — это элементы (набора) данных объекта JS или специального типа. Они используются для хранения информации о состоянии объекта. Внутренние методы — это функции-члены объекта JS.
Например, каждый объект JS имеет внутренний слот [[Prototype]] и внутренний метод [[GetOwnProperty]].
Внутренние слоты и методы недоступны в JS. Например, мы не можем получить доступ к o.[[Prototype]] или вызвать o.[[GetOwnProperty]](). Движок JS может выполнять их для собственных (внутренних) нужд, но не обязан делать этого.
Иногда внутренние методы становятся одноименными абстрактными операциями, как в случае с [[GetOwnProperty]]:
Когда внутренний метод [[GetOwnProperty]] объекта «O» вызывается с ключом «P», выполняются следующие действия:
- Вернуть ! OrdinaryGetOwnProperty(O, P)
OrdinaryGetOwnProperty — это не внутренний метод, поскольку он не связан с каким-либо объектом; объект, с которым он работает, передается ему в качестве параметра.
OrdinaryGetOwnProperty называется «обычным» (ordinary), поскольку он оперирует обычными объектами. Объекты в ECMAScript бывают обычными (ordinary) и необычными (экзотическими, exotic). Объект является обычным, если он ведет себя предсказуемо в ответ на набор методов, называемых основными внутренними методами (essential internal methods). В противном случае (когда объект ведет себя непредсказуемо; не так, как ожидается; когда поведение объекта отклоняется от нормального, является девиантным), он считается необычным.
Самым известным необычным объектом является Array, поскольку его свойство «length» ведет себя нестандартно: установление этого свойства может удалить элементы из массива.
Список основных внутренних методов можно посмотреть здесь.
Запись о завершении
Что насчет вопросительного и восклицательного знаков? Чтобы разобраться с этим, необходимо понять, что такое запись о завершении.
Запись о завершении — это специальный тип (определенный исключительно для целей спецификации). Движок JS не обязан иметь аналогичный внутренний тип данных.
Запись о завершении — это тип данных, имеющий фиксированный набор именованных полей. Запись о завершении имеет три поля:
[[Type]] | normal, break, continue, return или throw. Все типы, кроме normal, являются «внезапными (непреднамеренными) завершениями» (abrupt comlpetions) |
[[Value]] | Значение, полученное после завершения, например, значение, которое вернула функция, или выброшенное исключение |
[[Target]] | Используется для прямой управляемой передачи данных (не рассматривается в рамках данной статьи) |
Каждая абстрактная операция неявно возвращает запись о завершении. Даже если результатом абстрактной операции является простое логическое значение, оно оборачивается в запись о завершении с типом normal (см. Implicit Completion Values).
Примечание 1: спецификация не очень последовательна в этой части; существует несколько вспомогательных функций, возвращающих «голые» значения, которые используются как есть, без извлечения из записи о завершении.
Примечание 2: авторы спецификации стремятся сделать обработку записи о завершении более явной.
Если алгоритм выбрасывает исключение, это означает, что будет получена запись о завершении с типом ([[Type]]) throw и значением ([[Value]]) в виде объекта исключения. Мы пока не будем рассматривать другие типы (break, continue и return).
ReturnIfAbrupt(argument) означает выполнение следующих операций:
- Если argument - внезапный (abrupt), вернуть argument.
- Установить argument в argument.[[Value]].
Вот что из себя представляет запись о завершении; если завершается внезапно, сразу возвращаемся. В противном случае, извлекаем значение из записи о завершении.
ReturnIfAbrupt выглядит как вызов функции, но это не так. Мы вызываем функцию, которая возвращает ReturnIfAbrupt(), а не саму ReturnIfAbrupt. Ее поведение больше похоже на макрос в С-подобных языках программирования.
ReturnIfAbrupt может быть использована следующим образом:
- Пусть obj будет Foo() (obj - это запись о завершении).
- ReturnIfAbrupt(obj).
- Bar(obj) (если мы находимся здесь, значит, obj - это значение, извлеченное из записи о завершении).
Здесь в игру вступает вопросительный знак: запись ? Foo() эквивалента ReturnIfAbrupt(Foo()). Использование данного сокращения имеет практическую ценность: нам не нужно каждый раз писать код обработчика ошибок.
По аналогии, запись пусть val будет ! Foo() эквивалентна следующему:
- Пусть val будет Foo().
- Утверждение: val завершается нормально.
- Установить val в val.[[Value]].
Используя эти знания, мы можем переписать Object.prototype.hasOwnProperty следующим образом:
Object.prototype.hasOwnProperty(P)
- Пусть P будет ToProperty(V).
- Если P завершается внезапно, вернуть P.
- Установить P в P.[[Value]].
- Пусть O будет ToObject(значение this).
- Если 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 возвращает нечто, отличное от записи о завершении, это нечто неявно оборачивается в NormalCompletion.
Запасной вариант: Return ? Foo()
Спецификация использует нотацию Return ? Foo() — почему здесь вопросительный знак?
Запись Return ? Foo() может быть раскрыта следующим образом:
- Пусть temp будет Foo().
- Если temp завершается внезапно, вернуть temp.
- Установить temp в temp.[[Value]].
- Вернуть NormalCompletion.
Поведение Return ? Foo() одинаковое как для нормального, так и для внезапного завершения.
Запись Return ? Foo() позволяет более очевидным образом указать, что Foo возвращает запись о завершении.
Утверждения
Утверждения в спецификации «утверждают» инвариантные условия алгоритмов. Они добавлены в спецификацию для ясности, но не содержат никаких требований по реализации, поэтому не нуждаются в проверке со стороны конкретной реализации.
Что дальше?
Мы научились читать спецификацию по таким простым методам, как Object.prototype.hasOwnProperty, и таким абстрактным операциям, как HasOwnProperty. Имея эти знания, мы сможем понять, что делают другие абстрактные операции, о которых пойдет речь в следующей части. Также в следующей статье мы рассмотрим дескрипторы свойств (Property Descriptors), которые являются еще одним специальным типом.
Благодарю за внимание. Счастливого кодинга!
Автор: Igor Agapov