理解 ECMAScript 规范,第2部分
让我们继续练习我们出色的规范阅读技巧。如果你还没有查看前一集,现在是个好机会!
准备好进入第2部分了吗?
一个有趣的了解规范的方式是,从一个我们知道有的JavaScript功能开始,找出它是如何被规范定义的。
警告!本集包含从ECMAScript规范(截至2020年2月)中复制粘贴的算法。它们最终会过时。
我们知道属性是在原型链中查找的:如果一个对象没有我们尝试读取的属性,我们就沿着原型链向上查找,直到找到它(或者找到一个没有原型的对象)。
例如:
const o1 = { foo: 99 };
const o2 = {};
Object.setPrototypeOf(o2, o1);
o2.foo;
// → 99
原型链查找在哪里定义?
让我们来找出这种行为是在哪里定义的。一个好的起点是查看对象内部方法的列表。
既有[[GetOwnProperty]],也有[[Get]]——我们感兴趣的是不限于自身属性的版本,所以我们选择[[Get]]。
不幸的是,属性描述符规格类型也有一个名为[[Get]]的字段,因此在查看规范时,我们需要仔细区分这两种独立的用法。
[[Get]]是一个基本内部方法。普通对象实现了基本内部方法的默认行为。异质对象可以定义它们自己的[[Get]]内部方法,偏离默认行为。在本文中,我们专注于普通对象。
[[Get]]的默认实现委托给OrdinaryGet:
当
O的[[Get]]内部方法以属性键P和ECMAScript语言值Receiver被调用时,执行以下步骤:
- 返回
? OrdinaryGet(O, P, Receiver)。
我们很快会看到Receiver是调用访问器属性的getter函数时用作this值的值。
OrdinaryGet定义如下:
OrdinaryGet ( O, P, Receiver )当抽象操作
OrdinaryGet以对象O、属性键P和ECMAScript语言值Receiver被调用时,执行以下步骤:
- 断言:
IsPropertyKey(P)为true。- 令
desc为? O.[[GetOwnProperty]](P)。- 如果
desc为undefined,则
- 令
parent为? O.[[GetPrototypeOf]]()。- 如果
parent为null,返回undefined。- 返回
? parent.[[Get]](P, Receiver)。- 如果
IsDataDescriptor(desc)为true,返回desc.[[Value]]。- 断言:
IsAccessorDescriptor(desc)为true。- 令
getter为desc.[[Get]]。- 如果
getter为undefined,返回undefined。- 返回
? Call(getter, Receiver)。
原型链查找在步骤3中:如果我们没有找到作为自身属性的属性,我们调用原型的[[Get]]方法,该方法再次委托给OrdinaryGet。如果仍没有找到该属性,我们继续调用其原型的[[Get]]方法,其将再次委托给OrdinaryGet,以此类推,直到我们找到属性或到达没有原型的对象。
让我们看看当我们访问o2.foo时,这个算法是如何工作的。首先,我们调用OrdinaryGet,其中O为o2,P为"foo"。由于o2没有名为"foo"的自身属性,因此O.[[GetOwnProperty]]("foo")返回undefined,我们进入步骤3的分支。在步骤3.a中,我们将parent设置为o2的原型,即o1。parent不是null,因此我们未在步骤3.b退出。在步骤3.c中,我们调用父对象的[[Get]]方法,属性键为"foo",并返回其结果。
父对象(o1)是一个普通对象,因此其[[Get]]方法再次调用OrdinaryGet,这次O为o1,P为"foo"。o1有一个名为"foo"的自身属性,因此在步骤2中,O.[[GetOwnProperty]]("foo")返回关联的属性描述符,并将其存储在desc中。
属性描述符 是一种规范类型。数据属性描述符直接将属性的值存储在 [[Value]] 字段中。访问器属性描述符将访问器函数存储在 [[Get]] 和/或 [[Set]] 字段中。在本例中,与 "foo" 关联的属性描述符是一个数据属性描述符。
我们在步骤 2 中存储在 desc 中的数据属性描述符不是 undefined,因此我们不会在步骤 3 中走 if 分支。接下来我们执行步骤 4。属性描述符是一个数据属性描述符,因此我们在步骤 4 中返回其 [[Value]] 字段,值为 99,到此结束。
什么是 Receiver,它是从哪里来的?
Receiver 参数仅在步骤 8 中用于访问器属性的情况。当调用访问器属性的 getter 函数时,它作为 this 值 被传递。
OrdinaryGet 在整个递归过程中传递原始的 Receiver,不做修改(步骤 3.c)。让我们找出 Receiver 起初是从哪里来的!
通过搜索 [[Get]] 的调用位置,我们发现一个处理引用的抽象操作 GetValue。引用是一种规范类型,由基本值、引用的名称和严格引用标志组成。对于 o2.foo,基本值是对象 o2,引用的名称是字符串 "foo",由于示例代码是松散模式,严格引用标志为 false。
旁注:为什么引用不是记录?
旁注:引用不是记录,即使看起来可以是。它包含三个组成部分,这些组成部分也可以通过三个命名字段表示。引用不是记录,仅仅是由于历史原因。
回到 GetValue
让我们看看 GetValue 的定义:
ReturnIfAbrupt(V)。- 如果
Type(V)不是Reference,返回V。- 令
base为GetBase(V)。- 如果
IsUnresolvableReference(V)为true,抛出ReferenceError异常。- 如果
IsPropertyReference(V)为true,那么
- 如果
HasPrimitiveBase(V)为true,那么
- 断言:在此情况下,
base永远不会是undefined或null。- 将
base设置为! ToObject(base)。- 返回
? base.[[Get]](GetReferencedName(V), GetThisValue(V))。- 否则,
- 断言:
base是一个环境记录。- 返回
? base.GetBindingValue(GetReferencedName(V), IsStrictReference(V))。
在我们的示例中,引用是 o2.foo,它是一个属性引用。所以我们选择分支 5。我们不会选择分支 5.a,因为基本值(o2)不是一个原始值(如数字、字符串、符号、BigInt、布尔值、未定义或空)。
然后我们在步骤 5.b 中调用 [[Get]]。我们传递的 Receiver 是 GetThisValue(V)。在本例中,它只是引用的基本值:
- 断言:
IsPropertyReference(V)为true。- 如果
IsSuperReference(V)为true,那么
- 返回引用
V的thisValue组成部分的值。- 返回
GetBase(V)。
对于 o2.foo,我们不会选择步骤 2 的分支,因为它不是一个超级引用(如 super.foo),但我们选择步骤 3 并返回引用的基本值,即 o2。
将所有内容拼凑在一起,我们发现我们将 Receiver 设置为原始引用的基本值,然后在原型链遍历过程中保持不变。最终,如果我们找到的属性是一个访问器属性,我们在调用它时将 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 值 是我们最初尝试从中获取属性的对象,而不是找到该属性的对象。在本例中,this 值 是 o2 而不是 o1。我们可以通过检查 getter 返回的是 o2.x 还是 o1.x 来验证,确实,它返回的是 o2.x。
有效!我们能够根据规格中的内容预测此代码片段的行为。
访问属性 —— 为什么它会调用 [[Get]]?
规范在哪里规定当访问类似 o2.foo 的属性时会调用对象的内部方法 [[Get]]?肯定是在某处定义的。不要只是听我的话!
我们发现对象的内部方法 [[Get]] 是从抽象操作 GetValue 中调用的,而 GetValue 是用于处理引用的。但 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——这就是与我们示例相关的生成式。
生产式MemberExpression : MemberExpression . IdentifierName的运行时语义定义了评估它时需要采取的一系列步骤:
MemberExpression : MemberExpression . IdentifierName的运行时语义:评估
- 让
baseReference成为对MemberExpression评估的结果。- 让
baseValue成为? GetValue(baseReference)。- 如果匹配此
MemberExpression的代码是严格模式代码,则将strict设为true;否则将strict设为false。- 返回
? EvaluatePropertyAccessWithIdentifierKey(baseValue, IdentifierName, strict)。
该算法委托给抽象操作EvaluatePropertyAccessWithIdentifierKey,因此我们也需要阅读它:
EvaluatePropertyAccessWithIdentifierKey(baseValue, identifierName, strict)抽象操作
EvaluatePropertyAccessWithIdentifierKey接受值baseValue、解析节点identifierName和布尔参数strict作为参数。它执行以下步骤:
- 断言:
identifierName是一个IdentifierName。- 让
bv成为? RequireObjectCoercible(baseValue)。- 让
propertyNameString成为identifierName的StringValue。- 返回一个类型为
Reference的值,其基础值组件为bv,引用的名称组件为propertyNameString,其严格引用标志为strict。
也就是说:EvaluatePropertyAccessWithIdentifierKey构造了一个引用(Reference),使用提供的baseValue作为基础,identifierName的字符串值作为属性名称,并使用strict作为严格模式标志。
最终,这个引用会被传递给GetValue。根据引用的使用方式,在规范的多个地方定义了GetValue。
MemberExpression 作为一个参数
在我们的示例中,我们使用属性访问作为一个参数:
console.log(o2.foo);
在这种情况下,行为由ArgumentList生产式的运行时语义定义,该生产式会对参数调用GetValue:
ArgumentList : AssignmentExpression
- 让
ref成为对AssignmentExpression评估的结果。- 让
arg成为? GetValue(ref)。- 返回一个列表,其唯一项是
arg。
o2.foo看起来不像一个AssignmentExpression,但它确实是一个,因此此生产式是适用的。要了解原因,您可以查看这段额外内容,但此时您并不需要严格理解。
步骤1中的AssignmentExpression是o2.foo。ref即对o2.foo评估的结果,是上文提到的引用。在步骤2中,我们对它调用GetValue。因此,我们知道对象的内部方法[[Get]]将被调用,同时会进行原型链的查找。
总结
在本集,我们研究了规范如何定义一个语言功能(在此例中是原型查找),涵盖了所有层面:触发该功能的语法构造和定义该功能的算法。