- PVSM.RU - https://www.pvsm.ru -

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

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

Доброго времени суток, друзья!

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

Предисловие

Даже если вы хорошо знаете JavaScript, чтение спецификации [1] может быть затруднительным. Следующий код демонстрирует использование 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) [2]

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

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


… и

HasOwnProperty(O, P) [3]

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

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


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

Давайте это выясним.

Типы в языке и типы в спецификации

Начнем с чего-то знакомого. В спецификации встречаются такие значения, как undefined, true и false, которые известны нам по JS. Все они являются «языковыми значениями» (language values) [4], «значениями языковых типов» (values of language types) [5], которые также определяются спецификацией.

Спецификация использует встроенные языковые значения, например, внутренний тип данных может содержать поле со значением true или false. «Движки» JS обычно не используют языковые значения в своих внутренних механизмах. Например, если движок JS написан на C++, он скорее всего будет использовать true и false из C++, а не внутреннее представление булевых значений из JS.

В дополнение к языковым типам, в спецификации используются специальные типы (specification types), представляющие собой типы, которые используются только в спецификации, но не в языке JS. Движки JS не обязаны их выполнять (но могут). В данной статье мы познакомимся со специальным типом «Record» (запись) и его подтипом «Completion Record» (запись о завершении).

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

Абстрактные операции [6] — это функции, определенные в спецификации; они определяются в целях сокращения спецификации. Движки JS не обязаны выполнять их в качестве самостоятельных функций. В JS их нельзя вызывать напрямую.

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

Внутренние слоты и внутренние методы [7] обозначаются именами, заключенными в [[ ]].

Внутренние слоты — это элементы (набора) данных объекта JS или специального типа. Они используются для хранения информации о состоянии объекта. Внутренние методы — это функции-члены объекта JS.

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

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

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

[[GetOwnProperty]](P) [8]

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

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


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

OrdinaryGetOwnProperty называется «обычным» (ordinary), поскольку он оперирует обычными объектами. Объекты в ECMAScript бывают обычными (ordinary) и необычными (экзотическими, exotic). Объект является обычным, если он ведет себя предсказуемо в ответ на набор методов, называемых основными внутренними методами (essential internal methods). В противном случае (когда объект ведет себя непредсказуемо; не так, как ожидается; когда поведение объекта отклоняется от нормального, является девиантным), он считается необычным.

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

Список основных внутренних методов можно посмотреть здесь [9].

Запись о завершении

Что насчет вопросительного и восклицательного знаков? Чтобы разобраться с этим, необходимо понять, что такое запись о завершении [10].

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

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

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

Каждая абстрактная операция неявно возвращает запись о завершении. Даже если результатом абстрактной операции является простое логическое значение, оно оборачивается в запись о завершении с типом normal (см. Implicit Completion Values [11]).

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

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

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

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

  1. Если argument - внезапный (abrupt), вернуть argument.
  2. Установить argument в argument.[[Value]].


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

ReturnIfAbrupt выглядит как вызов функции, но это не так. Мы вызываем функцию, которая возвращает ReturnIfAbrupt(), а не саму ReturnIfAbrupt. Ее поведение больше похоже на макрос в С-подобных языках программирования.

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

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


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

По аналогии, запись пусть val будет ! Foo() эквивалентна следующему:

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


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

Object.prototype.hasOwnProperty(P)

  1. Пусть P будет ToProperty(V).
  2. Если P завершается внезапно, вернуть P.
  3. Установить P в P.[[Value]].
  4. Пусть O будет ToObject(значение this).
  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 возвращает нечто, отличное от записи о завершении, это нечто неявно оборачивается в NormalCompletion.

Запасной вариант: Return ? Foo()

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

Запись Return ? Foo() может быть раскрыта следующим образом:

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


Поведение Return ? Foo() одинаковое как для нормального, так и для внезапного завершения.

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

Утверждения

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

Что дальше?

Мы научились читать спецификацию по таким простым методам, как Object.prototype.hasOwnProperty, и таким абстрактным операциям, как HasOwnProperty. Имея эти знания, мы сможем понять, что делают другие абстрактные операции, о которых пойдет речь в следующей части. Также в следующей статье мы рассмотрим дескрипторы свойств (Property Descriptors), которые являются еще одним специальным типом.
Понимание спецификации ECMAScript, часть 1 - 2

Благодарю за внимание. Счастливого кодинга!

Автор: Igor Agapov

Источник [13]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/347920

Ссылки в тексте:

[1] спецификации: https://tc39.es/ecma262/

[2] Object.prototype.hasOwnProperty(V): https://tc39.es/ecma262/#sec-object.prototype.hasownproperty

[3] HasOwnProperty(O, P): https://tc39.es/ecma262/#sec-hasownproperty

[4] «языковыми значениями» (language values): https://tc39.es/ecma262/#sec-ecmascript-language-types

[5] «значениями языковых типов» (values of language types): https://tc39.es/ecma262/#sec-ecmascript-specification-types

[6] Абстрактные операции: https://tc39.es/ecma262/#sec-abstract-operations

[7] Внутренние слоты и внутренние методы: https://tc39.es/ecma262/#sec-object-internal-methods-and-internal-slots

[8] [[GetOwnProperty]](P): https://tc39.es/ecma262/#sec-ordinary-object-internal-methods-and-internal-slots-getownproperty-p

[9] здесь: https://tc39.es/ecma262/#table-5

[10] запись о завершении: https://tc39.es/ecma262/#sec-completion-record-specification-type

[11] Implicit Completion Values: https://tc39.es/ecma262/#sec-implicit-completion-values

[12] вопросительный знак: https://tc39.es/ecma262/#sec-returnifabrupt-shorthands

[13] Источник: https://habr.com/ru/post/490084/?utm_source=habrahabr&utm_medium=rss&utm_campaign=490084