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

Привет, Хабр! Представляю вашему вниманию перевод статьи под редакцией xfides


Автор оригинала: Marja Hölttä
Перевод первой части.



Давайте еще попрактикуемся в чтении спецификации. Если вы не видели предыдущую статью, самое время сходить ее посмотреть. В первой части мы познакомились с простым методом Object.prototype.hasOwnProperty. Также, посмотрели список абстрактных операций, которые вызываются при выполнении этого метода. Еще мы узнали о специфических сокращениях «?» и «!», которые связаны с обработкой ошибок. И наконец, мы получили информацию о типах языка, типах спецификации, внутренних слотах и внутренних методах.


Готовы для второй части?


Предупреждение! Этот эпизод содержит копию алгоритмов из спецификации ECMAScript от февраля 2020 года. Естественно, со временем информация будет устаревать.


Мы знаем, что свойства ищутся по прототипной цепочке: если объект не имеет свойства, которое пытаемся прочитать, мы будем подниматься вверх по прототипной цепочке, пока не найдем его, или пока не найдем объект, у которого не будет своего прототипа. Например:


const o1 = { foo: 99 };
const o2 = {};
Object.setPrototypeOf(o2, o1);
o2.foo;
// → 99

Где определяется алгоритм хождения по прототипной цепочке?


Давайте попытаемся найти, где это поведение определено. Хороший старт — начать со списка внутренних методов объекта.


Есть два метода [[GetOwnProperty]] и [[Get]]. Нам интересен тот, который умеет работать с прототипной цепочкой — это метод [[Get]]. К сожалению, дескриптор свойства в спецификации также имеет поле под названием [[Get]], поэтому, читая спецификацию, необходимо различать, когда речь идет о дескрипторе или об алгоритме.


[[Get]] — это базовый внутренний метод. Категория Обычных объектов реализуют поведение (алгоритм использования объекта) по умолчанию для базовых внутренних методов. Категория Экзотичных объектов могут определять свой собственный внутренний метод [[Get]], логика которого отличается от поведения по умолчанию. В этом посте мы сфокусируемся на обычных объектах.


Метод обычного объекта [[Get]] ( P, Receiver ). вызывает OrdinaryGet. При этом, когда вызывается внутренний метод [[Get]] от объекта «О» со свойством «Р» и ECMAScript значением Receiver, выполняется следующий шаг:


    1. Return ? OrdinaryGet(O, P, Receiver).

Вскоре мы увидим, что Receiver — это значение, которое используется как this значение, когда вызывается геттер функция свойства-акцессора.


OrdinaryGet(O, P, Receiver) определен следующим образом:


1.  Assert: IsPropertyKey(P) is true.
2.  Let desc be ? O.[[GetOwnProperty]](P).
3.  If desc is undefined, then
        a. Let parent be ? O.[[GetPrototypeOf]]().
        b. If parent is null, return undefined.
        c. Return ? parent.[[Get]](P, Receiver).
4.  If IsDataDescriptor(desc) is true, return desc.[[Value]].
5.  Assert: IsAccessorDescriptor(desc) is true.
6.  Let getter be desc.[[Get]].
7.  If getter is undefined, return undefined.
8.  Return ? Call(getter, Receiver).

Прототипная цепочка идет внутри третьего шага: если мы не нашли собственное свойство, мы вызываем прототипный [[Get]] метод, который снова вызывает OrdinaryGet. Если мы все еще не нашли свойство, мы вновь вызываем прототипный [[Get]] метод, который опять же передает вызов OrdinaryGet. Так продолжается до тех пор, пока мы не найдем свойство, или не достигнем объекта с прототипом равным null.


Давайте посмотрим, как этот алгоритм работает, когда мы обращаемся к o2.foo. Сначала мы вызываем OrdinaryGet и передаем в качестве параметра «О» объект «о2», а в качестве параметра «Р» — имя свойства «foo». После того, как O.[[GetOwnProperty]](«foo») возвращает undefined, мы идем в конструкцию if в шаге 3, поскольку объект «o2» не имеет собственного свойства под именем «foo».


В шаге 3.a, мы возвращаем в переменную «parent» ссылку на прототип объекта «o2» — это объект «o1». Так как «parent» не null, мы не проходим проверку if на шаге 3.b.


В шаге 3.с мы вызываем родительский метод [[Get]] с названием свойства «foo» и возвращаем его результат. Так как родительский объект «o1» — обычный объект, то метод [[Get]] вызывает OrdinaryGet снова. В этом случае, в качестве параметра «О» попадает «о1», а в «Р» передается «foo».


На шаге 2 метод O.[[GetOwnProperty]](«foo») возвращает ассоциированный со свойством дескриптор, и мы сохраняем его в переменную desc.


Дескриптор свойства — это тип спецификации. Дескрипторы данных хранят значения свойства непосредственно в поле [[Value]]. Дескриптор акцессор хранит функции акцессоры в поле [[Get]] и/или [[Set]]. В нашем случае, дескриптор свойства ассоциируемый с «foo» — это дескриптор данных.


Как вы помните, дескриптор свойства мы сохранили в desc на шаге 2, поэтому мы не проходим проверку if на шаге 3.


Далее мы выполняем шаг 4. Дескриптор свойства представлен дескриптором данных, поэтому вернется значение 99, которое лежит в поле [[Value]] на шаге 4. И на этом закончится алгоритм.


Что такое Receiver и откуда он взялся?


Параметр Receiver используется только в случае свойств-акцессора в шаге 8. Он передает значение this, когда вызывается геттер функция свойства-акцессора.


OrdinaryGet передает оригинальный Receiver через рекурсию без изменений (шаг 3.c). Давайте выясним, откуда взялся оригинальный Receiver.


На месте, где вызывается [[Get]], будет абстрактная операция GetValue, которая работает с Reference. Reference — это тип спецификации, содержащий базовое значение, указанное имя и флаг strict. В нашем случае с o2.foo базовое значение будет объект «o2», указанное имя — строка «foo», а флаг strict — false.


Отступление: почему тип Reference не является типом Record?


Тип Reference не является типом Record, как это могло бы быть. Он также содержит три компонента, каждый из которых может быть выражен через именованное поле. Но все же, тип Reference не тип Record только по историческим причинам.


Вернемся к GetValue.


Давайте посмотрим, как алгоритм GetValue ( V ) описан:


1.  ReturnIfAbrupt(V).
2.  If Type(V) is not Reference, return V.
3.  Let base be GetBase(V).
4.  If IsUnresolvableReference(V) is true, throw a ReferenceError exception.
5.  If IsPropertyReference(V) is true, then
     а.If HasPrimitiveBase(V) is true, then
         i.Assert: In this case, base will never be undefined or null.
         ii.Set base to ! ToObject(base).
     b.Return ? base.[[Get]](GetReferencedName(V),   GetThisValue(V)).
6.  Else,
      a.Assert: base is an Environment Record.
      b.Return ? base.GetBindingValue(GetReferencedName(V), IsStrictReference(V))

Reference в нашем примере это o2.foo, которое является property reference.


Итак, мы заходим в if на шаге 5. Не проходим проверку на шаге 5.a, поскольку объект «о2» не является примитивом (число, строка, символ, BigInt, Boolean, Undefined, или Null).


Затем мы вызываем [[Get]] на шаге 5.b. Receiver, который мы передаем — это значение, полученное после абстрактной операции GetThisValue(V). В нашем случае GetThisValue( V ) вернет значение базы Reference:


1.  Assert: IsPropertyReference(V) is true.
2.  If IsSuperReference(V) is true, then
        a.Return the value of the thisValue component of the reference V.
3.  Return GetBase(V).

Для нашего примера o2.foo, мы не проходим if на шаге 2, поскольку o2.foo не является Super Reference(как например super.foo), однако, выполняем шаг 3 и возвращаем значение базы Reference, которым является объект «o2».


Объединив все вместе, мы обнаружим, что мы задаем Receiver быть значением базы для оригинальной Reference, и, таким образом, мы сохраняем его неизменным во время прохождения по прототипной цепочки. И, в итоге, если свойство, которое мы нашли, является свойством-акцессором, мы используем Receiver как значение this.


В нашем случае, значение this внутри геттера ссылается на оригинальный объект, где мы пытались получить значение свойства, а не на тот, где мы нашли свойство во время прохождения по прототипной цепочке.


Давайте опробуем!


const o1 = { x: 10, get foo() { return this.x; } };
const o2 = { x: 50 };
Object.setPrototypeOf(o2, o1);
o2.foo;
// → 50

В этом примере у нас есть свойство-акцсессор под именем «foo», и мы определяем геттер для него. Геттер возвращает «this.x.».


Затем мы пытаемся получить значение o2.foo — что вернет геттер?


Мы обнаружили, что, когда мы вызываем геттер, значение this является объект, из которого мы изначально пытались получить свойство, а не объект, в котором мы его нашли. В нашем случае, значением this будет объект «о2», а не объект «о1». В этом мы можем удостоверится, проверив, что возвращает геттер: o2.x или o1.x. В действительности он возвращает o2.x.


Да, это работает! Мы смогли спрогнозировать поведение небольшого кусочка кода на основании того, что мы прочитали в спецификации.


Свойства — акцессоры — почему они вызывают [[Get]]?


Где в спецификации сказано, что внутренний метод объекта [[Get]] будет вызывается, когда получит доступ к свойству o2.foo? Конечно, это должно быть где-то описано. Не верьте мне на слово!


Мы нашли, что внутренний метод объекта [[Get]] вызвался из абстрактной операции GetValue, которая работает с References. Но откуда вызывается GetValue?


Семантика во время выполнения для MemberExpression


Грамматические правила спецификации определяют синтаксис языка. Семантика во время выполнения определяет смысл синтаксических конструкции, и то, как они будут выполнятся.


Если вы до сих пор не знакомы с контекстно-свободной грамматикой, самое время познакомится с нее сейчас.


Мы будем более глубоко разбирать грамматические правила в последующей главе, а пока давайте сохраним простоту. В частности, мы можем игнорировать индексы (Yield, Await и т.д.) в продукциях в нашем примере.


Следующие продукции описывают, как выглядит MemberExpression :


MemberExpression :
     PrimaryExpression
     MemberExpression [ Expression ]
     MemberExpression . IdentifierName
     MemberExpression TemplateLiteral
     SuperProperty
     MetaProperty
     new MemberExpression Arguments

Здесь представлены 7 продукций для MemberExpression.


MemberExpression может быть просто PrimaryExpression. Также MemberExpression может состоять из другого MemberExpression и Expression, соединенных вместе: MemberExpression[Expression], например o2[’foo’]. Или он может быть MemberExpression.IdentifierName, например o2.foo — это представление подходит для нашего примера.


Семантика во время выполнения для продукции Runtime Semantics: Evaluation for MemberExpression: MemberExpression. IdentifierName имеет следующим алгоритм:


1.  Let baseReference be the result of evaluating MemberExpression.
2.  Let baseValue be ? GetValue(baseReference).
3.  If the code matched by this MemberExpression is strict mode code, let strict be true; else let strict be false.
4.  Return ? EvaluatePropertyAccessWithIdentifierKey(baseValue, IdentifierName, strict).

Алгоритм переходит к абстрактной операции EvaluatePropertyAccessWithIdentifierKey, поэтому нам так же необходимо ее прочитать. Абстрактная операция EvaluatePropertyAccessWithIdentifierKey(baseValue, identifierName, strict) принимает в качестве аргументов значение baseValue, identifierName, а также strict и выполняет следующий алгоритм:


1.  Assert: identifierName is an IdentifierName
2.  Let bv be ? RequireObjectCoercible(baseValue).
3.  Let propertyNameString be StringValue of identifierName.
4.  Return a value of type Reference whose base value component is bv, whose referenced name component is propertyNameString, and whose strict reference flag is strict.

Таким образом, EvaluatePropertyAccessWithIdentifierKey создает Reference, который использует предоставленный baseValue в качестве base, строковое значение identifierName как имя свойства, и strict как флаг строго режима.


В итоге, Reference передается в GetValue. Это прописано в нескольких местах в спецификации, в зависимости от того, как Reference в конечном итоге будет использован.


MemberExpression как параметр


В нашем примере мы используем свойство доступа как параметр:


console.log(o2.foo);

В этом случае, используется продукция ArgumentList: AssignmentExpression. Дальше рассматривается ее поведение, определенное в алгоритме семантики для этой продукции. В нем вызывается GetValue для аргумента:


Runtime Semantics: ArgumentListEvaluation


1.  Let ref be the result of evaluating AssignmentExpression.
2.  Let arg be ? GetValue(ref).
3.  Return a List whose sole item is arg.

o2.foo не выглядит как AssignmentExpression, но это так и есть, поэтому применяем эту продукцию. Узнать, почему так, вы можете, прочитав эту дополнительную статью, но в этом нет острой необходимости сейчас.


В шаге 1 происходит вычисления алгоритма AssignmentExpression, которым является o2.foo. В ref попадет результат вычисления.


В шаге 2 мы вызываем GetValue от него. Таким образом, мы знаем, что внутренний метод объекта [[Get]] будет вызван, и обход по прототипной цепочки произойдет.


Резюме


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

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 0

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Самое читаемое