让我们继续练习这令人称赞的阅读标准文档的技能吧!如果你没有看过之前的文章,现在立马去看!
准备好了吗?
了解标准的一个有趣的方式是从已知的JavaScript特性开始,然后找出它是如何运作的。
警告: 本文中包含了一些从ECMAScript标准中复制的算法(2020.2)。它们最终会过时。
我们都知道对象上的属性会沿着原型链向上查找:如果一个对象不具有我们试图访问的属性,我们会沿着原型链查找这个属性直到我们找到这个属性或者找到原型链的尽头为止。
例如:
const o1 = { foo: 99 };
const o2 = {};
Object.setPrototypeOf(o2, o1);
o2.foo;
// → 99
原型链遍历在哪里定义的?
让我们来一起寻找这种沿着原型链查找属性的行为是在哪儿定义的。对象的内部方法列表是一个很好的开始查起的地方。
我们找到了两个属性:[[GetOwnProperty]]和[[Get]]属性,我们对这个不被自身属性(own properties)所约束的方法更感兴趣,所以我们先看看[[Get]]方法
[[Get]]是一个基本的内部方法。Ordinary objects为基本的内部方法实现了默认的行为。Exotic objects定义了从默认行为中推导出的属于它自己的内部方法。在本文中,我们主要聚焦于Ordinary objects。
对于[[Get]]方法的默认实现代理给了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:
- 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:
- Assert: IsPropertyKey(P) is true.
- Let desc be ? O.[GetOwnProperty].
- If desc is undefined, then
a. Let parent be ? O.[GetPrototypeOf].
b. If parent is null, return undefined.
c. Return ? parent.[[Get]](P, Receiver).
- If IsDataDescriptor(desc) is true, return desc.[[Value]].
- Assert: IsAccessorDescriptor(desc) is true.
- Let getter be desc.[[Get]].
- If getter is undefined, return undefined.
- Return ? Call(getter, Receiver).
遍历原型链在第3步:如果我们没有找到对象自身的属性,我们就调用代理给OrdinaryGet方法的对象原型的[[Get]]方法。如果我们还没有找到这个属性,我们继续调用这个原型的原型的[[Get]]方法,以此类推,直到我们找到这个属性或者找不到原型为止。
让我们继续观察在下面这段代码中我们读取o2.foo属性时代码是如何工作的。
const o1 = { foo: 99 };
const o2 = {};
Object.setPrototypeOf(o2, o1);
o2.foo;
// → 99
首先,我们会调用OrdinaryGet(O, P, Receiver)方法,这里O指o2, P指foo 。由于o2对象自身的属性上不存在foo属性。所以,调用O.[[GetOwnProperty]]("foo")会返回undefined,我们现在进入步骤3的if分支,在步骤3.a中,由于我们之前设置了o1作为o2的原型,o2的原型并不是null,所以我们跳过了步骤3.b, 在步骤3.c中,我们会调用o2原型的[[Get]]方法并返回。
o2的原型o1是一个ordinary object,所以它的[[Get]]方法会再次调用OrdinaryGet方法。这次,O是指o1, P是指foo。o1有一个自身的foo属性,所以在步骤2中, O.[[GetOwnProperty]]("foo") 返回了我们存储在desc中相关联的属性描述符。
属性描述符(Property Descriptor)是一个标准类型。Data Property Descriptor在[[Value]]域中直接保存了属性的值。Accessor Property Descriptor在[[Get]]/[[Set]]域中保存存取器函数。在这个例子中,与"foo"相关联的属性描述是data Property Descriptor。
在步骤2中,我们存储在desc中的data Property Descriptor不是undefined,所以我们不需要进入步骤3,我们直接进入步骤4,这个属性描述符刚好是data Property Descriptor,所以我们直接返回[[Value]]域,得到99,至此我们完成了属性的查找。
什么是Receiver?它从哪里来?
Receiver参数仅仅在步骤8中使用存取属性时用到。在存取器(getter/setter)中Receiver会被当作this值。
OrdinaryGet在递归的过程中不会修改Receiver。让我们看看Receiver到底时从哪里来的!
我们在Reference上发现了一个抽象操作GetValue,在这里[[Get]]方法被调用了,我们在这里看看能找到什么。Reference是specification type,它由base value, referenced name和strict reference flag构成。在o2.foo这个例子中,o2是base value, foo是referenced name,strict refernce flag是false
为什么Reference不是Record?
Reference类型不是Record类型,即使它们两个看起是如此的像。它包含三个组成部分,这同样可以表示为三个字段。Reference不是Record类型仅仅是因为历史原因。
回到GetValue
我们来看看GetValue是如何定义的:
- ReturnIfAbrupt(V).
- If Type(V) is not Reference, return V.
- Let base be GetBase(V).
- If IsUnresolvableReference(V) is true, throw a ReferenceError exception.
- 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)).
- Else,
Assert: base is an Environment Record. Return ? base.GetBindingValue(GetReferencedName(V), IsStrictReference(V))
在本例中,我们的Reference类型数据是o2.foo,它是一个property reference。所以我们查看分支5,由于o2的base value不是一个基本类型数据(Number, String, Symbol, BigInt, Boolean, Undefined, Null),所以我们不会进入分支5.a
我们会在分支5.b中调用[[Get]]方法。我们所传递的Receiver是GetThisValue(V)。在本例中,它就是指Reference的base value:
GetThisValue( V )
- Assert: IsPropertyReference(V) is true.
- If IsSuperReference(V) is true, then
a. Return the value of the thisValue component of the reference V.
- Return GetBase(V).
对于o2.foo,因为不存在Super Reference(例如:super.foo)我们不会选择步骤2的分支。我们会选择步骤3的分支并且返回Reference的base value,就是o2
把上述的知识点汇集在一起,我们慢慢思考一下。我们发现我们将Receiver设置为了原始Reference的base,并且在沿着原型链向上查找的过程中它都保持不变。最终,我们发现我们要找的这个属性是一个存取器,当存取器被调用时,我们就会使用Receiver作为其中的this值。
尤其是在getter中,this值指的是我们最先尝试获得属性的对象,而不是原型链上的对象。
Let's try it out!
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这个属性,最后存取器返回了什么?
我们发现当我们调用getter时,它的this值是我们最先尝试获取foo属性值的对象,而不是我们在原型链上所找到的有这个属性的对象。在本例中,this值是o2,而不是o1。我们通过最后的返回值返回了o2.x而不是o1.x确认了这一点。
我们基于我们阅读的标准正确的预测了上面的代码,Amazing!
访问属性——为什么会调用[[Get]]
在标准中哪一部分说了当我们访问一个类似于o2.foo的属性时,对象的内部方法[[Get]]会被调用?值得肯定的是这在某一个地方真的被定义了。
我们发现对象的内部方法[[Get]]是在Reference的抽象操作GetValue中被调用的。但是GetValue又是被谁调用了?
MemberExpression的运行时语义
标准的语法规则(grammer rules)定义了语言的语法(language syntax)。运行时的语义决定了语法结构表示的意义(在运行时中如何确定它们的意义)
如果你对上下文无关文法不熟悉,现在正是一个了解它的好时机。
我们在后面的文章中会深入的探究语法规则,但是现在让我们尽可能让它简单化!我们现在可以忽略本文中的产生式的一些下标(Yield, Await等等)。
下面的产生式描述了MemberExpression的文法:
MemberExpression :
PrimaryExpression
MemberExpression [ Expression ]
MemberExpression . IdentifierName
MemberExpression TemplateLiteral
SuperProperty
MetaProperty
new MemberExpression Arguments
对于MemberExpression这里有7个产生式。一个MemberExpression可以是PrimaryExpression,它也可以是由其他的MemberExpression和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 evaluating MemberExpression.
- Let baseValue be ? GetValue(baseReference).
- If the code matched by this MemberExpression is strict mode code, let strict be true; else let strict be false.
- 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:
- Assert: identifierName is an IdentifierName
- Let bv be ? RequireObjectCoercible(baseValue).
- Let propertyNameString be StringValue of identifierName.
- 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作为strict mode flag。
最终,这个被构建出来的Reference被传递给了GetValue。这在标准的好几个地方都有定义,最终还是取决于Reference的使用方式。
MemberExpression作为参数
在这个例子中,我们使用一个对象的属性作为参数
console.log(o2.foo);
本例中,它的行为被定义在ArgumentList的在argument调用GetValue的产生式的运行时语义中:
Runtime Semantics: ArgumentListEvaluation
ArgumentList : AssignmentExpression
- Let ref be the result of evaluating AssignmentExpression.
- Let arg be ? GetValue(ref).
- Return a List whose sole item is arg.
o2.foo看起来并不是一个AssignmentExpression,但是它的确是。所以这个产生式是适用的。为了找到为什么如此的原因,你可以看看这篇文章,但对于这一里的内容不是必须的。
在步骤1中,AssignmentExpression是指o2.foo.ref,o2.foo求值的结果就是上面提到的Reference。步骤2中我们调用了GetValue。这样,我们就知道对象的内部方法[[Get]]被调用了,并且也会开始遍历原型链。
总结
本文中,我们了解了不标准如何跨不同层定义语言特性:触发该特性的结构和算法定义了它。