如何阅读 ECMAScript 规范 - 第二部分

133 阅读9分钟

[原文链接](Understanding the ECMAScript spec, part 2 · V8)

让我们进一步练习规范阅读技巧,还没有阅读过第一部分的请点击这里。如何阅读 ECMAScript 规范 - 第一部分 - 掘金 (juejin.cn)

开始第二部分?(Ready for part2?)

为了避免阅读规范产生枯燥乏味的感觉,我们可以从已知的 JavaScript 特性开始,探索其在规范中是如何运作的。

警告!本系列的算法来自2020年2月的规范,会与最新的版本存在差异。

我们知道属性是在原型链中查找的:如果一个对象没有我们想要的属性,我们就沿着原型链往上走,直到找到该属性(或者找到一个不再有原型的对象)。

比如:

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

原型链查找是在哪定义的?(Where’s the prototype walk defined?)

让我们看看原型链查找这种行为是在哪定义的。对象内部方法 列表是一个很好的切入点。

我们感兴趣的是 [[GetOwnProperty]] 和 [[Get]], 但不局限于对象自身的属性,因此我们选择从 [[Get]] 开始。

令人诅丧的是,属性描述符规范类型 也有 [[Get]] 字段,所以我们在查阅规范中的 [[Get]] 时,需要细心分辨这两者的用法。

[[Get]] 属于基本内部方法。普通对象实现了基本内部方法的默认行为。奇异对象可以自定义与默认行为不同的内部方法 [[Get]]。本文我们关注的是普通对象。

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

我们很快就会看到,当调用访问器属性的 getter 函数时,Receiver 表示的是 this 值。

OrdinaryGet 的定义如下:

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

原型链遍历在第3步:如果我们在对象上没有找到目标属性就调用该对象原型上的 [[Get]] 方法,该方法会再次委托给 OrdinaryGet。如果还是没找到目标属性,我们继续调用该原型对象原型上的 [[Get]] 方法并再次委托给 OrdinaryGet,以此类推。直到找到目标属性或是到达原型链的顶端为止。

让我们看看访问 o2.foo 时这个算法是如何运转的。首先我们调用 OrdinaryGet, O 是 o2, P 是 foo。o2.[GetOwnProperty] 会返回 undefined, 因为 o2 本身没有名为 "foo" 的属性,所以我们进入到步骤3的 if 分支。在步骤3.a中,我们将 o2 的原型 - o1 设置为 parent。parent 不为 null,因此跳过步骤3.b。在步骤3.c中,我们调用 parent 的 [[Get]] 方法,并将 "foo" 作为入参 P,最终返回其值。

parent (o1) 是一个普通对象,所以其 [[Get]] 方法会再次调用 OrdinaryGet,此时,O 是 o1, P 是 foo。o1 本身有 foo 属性,因此在步骤2中,[[GetOwnProperty]] ("foo") 返回相关的属性描述符,并将其存储在 desc 中。

Property Descriptor 是规范类型。数据属性描述符(Data Property Descriptors)直接将属性值存储在 [[value]] 字段中。访问属性描述符(Accessor Property Descriptor)的访问器函数存储在 [[Get]] 和 [[Set]] 字段中。在上面的示例中,和 "foo" 相关联的属性描述符是一个数据属性描述符。

我们在第2步中存储的数据属性描述符 desc 不是 undefined,所以我们不会进到步骤3的 if 分支。接下来执行步骤4。属性描述符是一个数据属性描述符,所以我们在步骤4中返回它的 [[Value]] 字段 99,这样整个算法就结束了。

什么是 Receiver,它是哪来的?(What’s Receiver and where is it coming from?)

Receiver 参数在步骤8中的访问器属性有用到。当调用访问器属性的 getter 函数时,它作为 this 值传递。

OrdinaryGet 在整个递归过程中传递初始的 Receiver,且未更改(步骤3.c)。让我们来看看 Receiver 最初来自哪里!

在搜索 [[Get]] 被调用的地方时,我们发现有一个抽象操作 GetValue,它对引用(References)进行操作。References 是一种规范类型,由基值、引用名称和严格引用标志组成。在 o2.foo 的例子中,基值是对象 o2,引用名称是 foo,严格引用标志是 false,因为示例代码是非严格环境。

为什么引用不是记录?(Why is Reference not a Record?)

引用并不是记录,尽管听起来像。它包含三个组成部分,可以同样地表示为三个命名字段。引用不是记录仅仅是因为历史原因。

回到 GetValue(Back to GetValue

让我们看看 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
    a. If HasPrimitiveBase(V) is true, then
    1. Assert: In this case, base will never be undefined or null.
    2. 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))

我们示例中的引用是 o2.foo,它是一个属性引用。所以我们进到步骤5,跳过5.a,因为 base(o2) 不是基本值(Number, String, Symbol, BigInt, Boolean, Undefined, or Null)。

接着,我们调用步骤5.b中的 [[Get]]。GetThisValue(V) 就是我们传递的 Receiver,在本例中,它就是引用的基值:

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.
  3. Return GetBase(V).

对于 o2.foo,我们不会进到步骤2,因为它不是 Super 引用(例如 super.foo),但我们会进到步骤3并返回引用的基值。

将所有内容拼凑起来,会发现我们将 Receiver 设置成了原始 Reference 的基值,然后在原型链遍历过程中保持其不变。最后,如果我们找到的属性是一个访问器属性,我们在调用它时使用 Receiver 作为 this 值。
特别地,getter 中的 this 值指向我们试图获取属性的原始对象,而不是在原型链遍历过程中发现存在该属性的那个对象。

让我们试试:

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

在这个例子中,我们有一个名为 foo 的访问器属性,并为它定义了一个 getter。getter 返回 this.x。

然后我们获取 o2.foo - getter 会返回什么呢?

我们发现,当我们调用 getter 时,this 值是我们最初试图获取属性的对象,而不是我们在原型链上找到它的对象。在这个例子中,这个值是 o2,而不是 o1。我们可以通过检查 getter 的返回值是 o2.x 还是 o1.x 来验证这点,事实是,返回 o2.x。

获取属性 - 为什么会调用 [[Get]]? - (Accessing properties — why does it invoke [[Get]]?)

当获取像 o2.foo 这样的属性时,规范在哪里提到了对象的内部方法 [[Get]] 将会被调用?这肯定是有定义的。

我们发现 Object 的内部方法 [[Get]] 是由抽象操作 GetValue 调用的,GetValue 操作的是 References。但 GetValue 是在哪里调用的呢?

成员表达式的运行时语义(Runtime semantics for MemberExpression)

规范中的语法规则定义了语言的语法。运行时语义 定义了这些语法结构的"含义"(如何在运行时对它们求值)。

如果你还不熟悉 上下文无关语法,现在最好去看看!

我们将在后面的文章中深入了解语法规则,现在我们只需要简单的入门!特别的,我们在本文忽略了产品集(productions)中原有的下标(Yield、Await等)。

下面的产品集具体描述了 MemberExpression

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

这里的成员表达式有7个产品。成员表达式可以是基本表达式(PrimaryExpression)。或者,可以由另一个成员表达式和表达式(Expression)构成,将它们拼接在一起:MemberExpression [Expression],比如 o2['foo']。再或者可以是 MemberExpression . IdentifierName,比如 o2.foo - 这是与我们示例相关的产品。

对于产品 MemberExpression : MemberExpression . IdentifierName,运行时语义定义了在求值时需要执行的算法步骤。

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 )

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 构造了一个 Reference,它使用入参中的 baseValue 作为基值,identifierName 的字符串值作为属性名,而 strict 则作为严格模式标志。

最终这个 Reference 被传递给 GetValue。这在规范的好几个地方都有定义,取决于 Reference 最终如何被使用。

成员函数作为参数(MemberExpression as a parameter)

在我们的示例中使用属性访问作为参数:

console.log(o2.foo);

在本例中,该行为是在 ArgumentList 产品的运行时语义中定义的,该产品会在参数上调用 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 看起来不像一个 AssignmentExpression,但它确实是,所以这个生产是适用的。要搞明白为什么,可以查看这些 额外内容,但在这里并不是必要的。

第1步中的 AssignmentExpression 是 o2.foo。o2.foo 的结果 ref,是上面提到的 Reference。在步骤2中,我们调用 GetValue。因此,我们就知道了 Object 的内部方法[[Get]] 将会被调用,并且将发生原型链遍历。

总结

在本文中,我们看到了规范是如何定义语言特性的,在原型链查找的示例中,涵盖了所有不同的要素:触发该特性的语法结构以及定义它的算法。