[原文链接](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]]。本文我们关注的是普通对象。
When the
[[Get]]
internal method ofO
is called with property keyP
and ECMAScript language valueReceiver
, the following steps are taken:
- Return
? OrdinaryGet(O, P, Receiver)
.
我们很快就会看到,当调用访问器属性的 getter 函数时,Receiver 表示的是 this 值。
OrdinaryGet 的定义如下:
OrdinaryGet ( O, P, Receiver )
When the abstract operation
OrdinaryGet
is called with ObjectO
, property keyP
, and ECMAScript language valueReceiver
, the following steps are taken:
- Assert:
IsPropertyKey(P)
istrue
.- Let
desc
be? O.[[GetOwnProperty]](P)
.- If
desc
isundefined
, then
a. Letparent
be? O.[[GetPrototypeOf]]()
.
b. Ifparent
isnull
, returnundefined
.
c. Return? parent.[[Get]](P, Receiver)
.- If
IsDataDescriptor(desc)
istrue
, returndesc.[[Value]]
.- Assert:
IsAccessorDescriptor(desc)
istrue
.- Let
getter
bedesc.[[Get]]
.- If
getter
isundefined
, returnundefined
.- 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 的定义:
ReturnIfAbrupt(V)
.- If
Type(V)
is notReference
, returnV
.- Let
base
beGetBase(V)
.- If
IsUnresolvableReference(V)
istrue
, throw aReferenceError
exception.- If
IsPropertyReference(V)
istrue
, then
a. IfHasPrimitiveBase(V)
istrue
, then
- Assert: In this case,
base
will never beundefined
ornull
.- Set
base
to! ToObject(base)
. b. Return? base.[[Get]](GetReferencedName(V), GetThisValue(V))
.- 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,在本例中,它就是引用的基值:
- Assert:
IsPropertyReference(V)
istrue
.- If
IsSuperReference(V)
istrue
, then
a. Return the value of thethisValue
component of the referenceV
.- 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
- Let
baseReference
be the result of evaluatingMemberExpression
.- Let
baseValue
be? GetValue(baseReference)
.- If the code matched by this
MemberExpression
is strict mode code, letstrict
betrue
; else letstrict
befalse
.- Return
? EvaluatePropertyAccessWithIdentifierKey(baseValue, IdentifierName, strict)
.
算法会委托给抽象操作 EvaluatePropertyAccessWithIdentifierKey
EvaluatePropertyAccessWithIdentifierKey( baseValue, identifierName, strict )
The abstract operation
EvaluatePropertyAccessWithIdentifierKey
takes as arguments a valuebaseValue
, a Parse NodeidentifierName
, and a Boolean argumentstrict
. It performs the following steps:
- Assert:
identifierName
is anIdentifierName
- Let
bv
be? RequireObjectCoercible(baseValue)
.- Let
propertyNameString
beStringValue
ofidentifierName
.- Return a value of type Reference whose base value component is
bv
, whose referenced name component ispropertyNameString
, and whose strict reference flag isstrict
.
即:EvaluatePropertyAccessWithIdentifierKey 构造了一个 Reference,它使用入参中的 baseValue 作为基值,identifierName 的字符串值作为属性名,而 strict 则作为严格模式标志。
最终这个 Reference 被传递给 GetValue。这在规范的好几个地方都有定义,取决于 Reference 最终如何被使用。
成员函数作为参数(MemberExpression
as a parameter)
在我们的示例中使用属性访问作为参数:
console.log(o2.foo);
在本例中,该行为是在 ArgumentList 产品的运行时语义中定义的,该产品会在参数上调用 GetValue
Runtime Semantics:
ArgumentListEvaluation
ArgumentList : AssignmentExpression
- Let
ref
be the result of evaluatingAssignmentExpression
.- Let
arg
be? GetValue(ref)
.- Return a List whose sole item is
arg
.
o2.foo 看起来不像一个 AssignmentExpression,但它确实是,所以这个生产是适用的。要搞明白为什么,可以查看这些 额外内容,但在这里并不是必要的。
第1步中的 AssignmentExpression 是 o2.foo。o2.foo 的结果 ref,是上面提到的 Reference。在步骤2中,我们调用 GetValue。因此,我们就知道了 Object 的内部方法[[Get]] 将会被调用,并且将发生原型链遍历。
总结
在本文中,我们看到了规范是如何定义语言特性的,在原型链查找的示例中,涵盖了所有不同的要素:触发该特性的语法结构以及定义它的算法。