Let’s continue to practice this admirable skill of reading standard documents! If you haven’t read the previous article, do it now!

How to read the ECMAScript standard — part1

Are you ready?

An interesting way to learn about standards is to start with known JavaScript features and find out how it works.

Warning: This article contains some algorithms copied from the ECMAScript standard (2020.2). They will eventually become obsolete.

We all know that properties on objects look up the stereotype chain: if an object doesn’t have a property we’re trying to access, we look up that property along the stereotype chain until we find that property or the end of the stereotype chain.

Such as:

const o1 = { foo: 99 };
const o2 = {};
Object.setPrototypeOf(o2, o1);
o2.foo;
/ / - 99
Copy the code

Where is prototype chain traversal defined?

Let’s find out where this behavior of looking up attributes along the stereotype chain is defined. Object’s internal method list is a good place to start. We found two properties: [[GetOwnProperty]] and [[Get]]. We’re more interested in methods that aren’t bound by their own properties, so let’s look at the [[Get]] method first

[[Get]] is a basic internal method. Ordinary Objects implements the default behavior as a basic internal method. Exotic Objects defines its own internal methods derived from the default behavior. In this article, we mainly focus on Ordinary objects.

The default implementation proxy for the [[Get]] method is given OrdinaryGet

[[Get]] ( P, Receiver )

When the [[Get]] internal method of O is called with property key P and ECMAScript language value Receiver, the following steps are taken:

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

As we will see in a moment, the value of Receiver is used as the this value when the getter function is called.

OrdinaryGet is defined as follows:

OrdinaryGet ( O, P, Receiver )

When the abstract operation OrdinaryGet is called with Object O, property key P, and ECMAScript language value Receiver, the following steps are taken:

  1. Assert: IsPropertyKey(P) is true.
  2. Let desc be ? O.[GetOwnProperty].
  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).

  1. If IsDataDescriptor(desc) is true, return desc.[[Value]].
  2. Assert: IsAccessorDescriptor(desc) is true.
  3. Let getter be desc.[[Get]].
  4. If getter is undefined, return undefined.
  5. Return ? Call(getter, Receiver).

Walking through the prototype chain is at step 3: if we do not find the property of the object itself, we call the [[Get]] method of the object prototype that is proxyed to the OrdinaryGet method. If we haven’t found the property yet, we continue to call the prototype’s [[Get]] method, and so on, until we either find the property or can’t find the prototype.

Let’s continue to observe how the code works when we read the o2.foo property in the following code.

const o1 = { foo: 99 };
const o2 = {};
Object.setPrototypeOf(o2, o1);
o2.foo;
/ / - 99
Copy the code

First, we call the OrdinaryGet(O, P, Receiver) method, where O means o2 and P means foo. Foo does not exist on the o2 object’s own property. So, calling O.[[GetOwnProperty]](“foo”) returns undefined, we now go to the if branch of Step 3. In Step 3.a, since we set O1 as the prototype for O2, the prototype for O2 is not null. So we skip step 3.b, and in step 3.c we call the [[Get]] method of the O2 prototype and return.

The prototype o1 of O2 is an ordinary Object, so its [[Get]] method calls the OrdinaryGet method again. This time, O refers to o1 and P refers to foo. O1 has a foo property of its own, so in Step 2, O.[[GetOwnProperty]](“foo”) returns the property descriptor we stored associated with DESC.

A Property Descriptor is a standard type. The Data Property Descriptor stores Property values directly in the [[Value]] field. Accessor Property Descriptor stores Accessor functions in the [[Get]]/[[Set]] fields. In this case, the Property description associated with “foo” is the data Property Descriptor.

So in step 2, the data Property Descriptor that we store in desc is not undefined, so we don’t need to go to step 3, we just go to step 4, and this Property Descriptor happens to be the data Property Descriptor, So we go straight back to the [[Value]] field and get 99, and we’re done looking for the attributes.

What is Receiver? Where does it come from?

The Receiver parameter is used only when the access properties are used in Step 8. In accessors (getters/setters), Receiver is treated as this.

OrdinaryGet does not modify Receiver during recursion. Let’s see where Receiver actually comes from!

We find an abstract operation GetValue on Reference, where the [[Get]] method is called, and let’s see what we can find there. Reference is specification type, which consists of Base value, referenced Name and strict Reference Flag. In the example o2.foo, O2 is base value, foo is referenced name, and strict Refernce flag is false

Why is Reference not a Record?

The Reference type is not a Record type, even though the two look so similar. It contains three components, which can also be represented as three fields. Reference is not of Record type only for historical reasons.

Back to the GetValue

Let’s look at how GetValue is defined:

  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

a. 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)).

  1. Else,

Assert: base is an Environment Record. Return ? base.GetBindingValue(GetReferencedName(V), IsStrictReference(V))

In this case, our Reference type data is O2.foo, which is a property Reference. So we look at branch 5, because the base value of O2 is not a basic type data (Number, String, Symbol, BigInt, Boolean, Undefined, Null), we do not enter branch 5

We call the [[Get]] method on branch 5.b. The Receiver we pass is GetThisValue(V). In this case, it refers to the base value of Reference:

GetThisValue( V )

  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.

  1. Return GetBase(V).

For O2.foo, we do not select the branch of Step 2 because there is no Super Reference (e.g. Super.foo). We will select the branch of step 3 and return the base value of Reference, which is O2

Put all these together, let’s think about it. We find that we set the Receiver as the base of the original Reference and it stays the same as we look up the prototype chain. Finally, we find that the property we are looking for is an accessor, and when the accessor is called, we use Receiver as the this value.

Especially in getters, the this value refers to the object we first try to get the property from, not the object on the prototype chain.

Let’s try it out!

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

In this case, there is an accessor property foo that returns the value of this.x

Then, we access o2. Foo, and what does the accessor return?

We found that when we called the getter, its this value was the first object we tried to get the value of the property foo, not the object we found on the prototype chain with that property. In this case, the value is O2, not o1. We confirm this by returning O2.x instead of o1.x with the final return value.

We correctly predicted the above code based on the criteria we read, Amazing!

Access properties – why are they called[[Get]]

Where in the standard does it say that when we access a property like O2.foo, the object’s internal method [[Get]] is called? To be sure, this is really defined somewhere.

We find that the object’s internal method [[Get]] is called in Reference’s abstract operation GetValue. But who is GetValue called by?

Runtime semantics of MemberExpression

Standard grammer rules define language syntax. The semantics of the runtime determine the meaning of the representation of syntactic structures (how their meaning is determined at run time)

If you are not familiar with context-free grammars, now is a good time to learn about them.

We’ll delve deeper into grammar rules in a later article, but for now let’s keep it as simple as possible! We can now ignore some of the subscripts (Yield, Await, and so on) of the production in this article.

The following production describes the grammar of MemberExpression:

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

There are seven production expressions for MemberExpression. A MemberExpression can be PrimaryExpression, or it can be a combination of other memberexpressions and expressions: MemberExpression[Expression], for example: O2 [‘foo’] is of this form. Or is using MemberExpression IdentifierName such forms, such as o2. Such is the foo. These are the relevant production expressions in this example.

For MemberExpression: MemberExpression. IdentifierName, the following steps are used to determine its meaning:

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).

The above algorithm gave the agent EvaluatePropertyAccessWithIdentifierKey this abstract operation, so we still need further reading:

EvaluatePropertyAccessWithIdentifierKey( baseValue, identifierName, strict ) The abstract operation EvaluatePropertyAccessWithIdentifierKey takes as arguments a value baseValue, a Parse Node identifierName, and a Boolean argument strict. It performs the following steps:

  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 constructed a Reference, it USES baseValue as base, IdentifierName as the property name, strict as strict mode flag.

Finally, the constructed Reference is passed to GetValue. This is defined in several places in the standard, but ultimately depends on how Reference is used.

MemberExpression as a parameter

In this example, we use the attributes of an object as arguments

console.log(o2.foo);
Copy the code

In this case, its behavior is defined in the runtime semantics of the ArgumentList production of the argument call to GetValue:

Runtime Semantics: ArgumentListEvaluation

ArgumentList : AssignmentExpression

  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 does not look like an AssignmentExpression, but it is. So this production works. To find out why this is so, you can check out this article, but it is not necessary for the content in this section.

In step 1, AssignmentExpression refers to O2.foo. ref, and the result of evaluation of O2.foo is the Reference mentioned above. In Step 2, we called GetValue. This way, we know that the object’s internal method [[Get]] has been called, and we will also start traversing the prototype chain.

conclusion

In this article, we saw how nonstandards define a language feature across different layers: the structures and algorithms that trigger the feature define it.

The original address