理解 ECMAScript 规范(二)
- 原文地址:Understanding the ECMAScript spec, part 2
- 原文作者:marjakh
- 译文出自:IDuxFE 技术文章翻译
- 本文永久链接:github.com/IDuxFE/week…
- 译者:Levix
第二部分,了解 ECMAScript 规范
2020 年 3 月 2 日发布 · 标签:ECMAScript 了解 ECMAScript
让我们再多练习一下阅读规范的技巧,如果你还没有看过上一集,现在正是学习它的好时机!
准备好第二部分的学习了吗?#
一个了解规范的好方法是从我们熟知的 JavaScript 特性开始,并弄清楚它是如何被定义的。
警告! 截止至 2020 年 2 月,本集包含的算法是从 ECMAScript 规范 中复制粘贴的,它们最终会被废弃的。
我们知道,属性是通过原型链查找得到的:如果一个对象没有我们要读取的属性,我们会沿着原型链向上查找,直到找到它(或者找到一个不再有原型的对象)。
例如:
const o1 = { foo: 99 };
const o2 = {};
Object.setPrototypeOf(o2, o1);
o2.foo;
// → 99
原型路径的定义在哪里?#
我们一起尝试找出这种行为是在哪里定义的,可以从对象的内部方法列表开始。
(每个对象)都包含 [[GetOwnProperty]] 和 [[Get]] (两个基本内部方法) —— 我们感兴趣的是不限制于 own 属性的版本,所以我们将选择 [[Get]] 。
遗憾的是,Property Descriptor 规范类型也有一个叫做 [[Get]] 的字段,所以在浏览 [[Get]] 的规范时,我们需要仔细区分这两种各自的使用方式。
[[Get]] 是一个基本的内部方法,普通对象实现了基本内部方法的默认行为。外来对象可以定义自己的内部方法 [[Get]] ,但它偏离了默认行为。在这篇文章中,我们专注于讨论普通对象。
对 [[Get]] 的默认实现委托给了 OrdinaryGet :
当
O的内部方法[[Get]]被属性键为P以及 ECMAScript 语言值为Receiver调用时,将执行以下步骤:
- 返回
? OrdinaryGet(O, P, Receiver).
我们很快就会看到, Receiver 是在调用访问器属性的 getter 函数时,被用作 this 值。
OrdinaryGet 定义如下:
OrdinaryGet ( O, P, Receiver )在以对象
O、属性键P和 ECMAScript 语言值Receiver调用抽象操作OrdinaryGet时,将执行以下步骤:
- 断言:
IsPropertyKey(P)为true;- 令
desc为? O.[[GetOwnProperty]](P);- 若
desc为undefined,则 a. 令parent为? O.[[GetPrototypeOf]](); b. 若parent为null,返回undefined; c. 返回? parent.[[Get]](P, Receiver);- 若
IsDataDescriptor(desc)为true, 返回desc.[[Value]];- 断言:
IsAccessorDescriptor(desc)为true;- 令
getter为desc.[[Get]];- 若
getter为undefined,返回undefined;- 返回
? Call(getter, Receiver)。
原型链的查找行为在步骤 3 里面:如果我们没找到自有属性(desc),调用原型的 [[Get]] 方法,并再次委托(抽象操作) OrdinaryGet 。假如我们依然没有找到该属性,继续调用原型的 [[Get]] 方法,又会再次委托 OrdinaryGet ,以此类推,直到找到该属性或者遇到一个没有原型的对象为止。
让我们看看当我们访问 o2.foo 时,该算法是如何工作的。首先我们令 O 为 o2 ,P 为 "foo" 去调用 OrdinaryGet , O.[[GetOwnProperty]]("foo") 返回 undefined ,因为 o2 没有名为 "foo" 的自有属性,所以我们在步骤 3 采用 if 分支。在步骤 3.a 中,我们将 parent 设置为 o2 的原型,即 o1 ,parent 不为 null,因此在步骤 3.b 中不会直接返回。在步骤 3.c 中,我们调用 parent 的 [[Get]] 方法,传入 "foo" ,并返回最终结果。
parent(o1)是一个普通对象,所以它的 [[Get]] 方法会再次调用 OrdinaryGet,这一次 O 为 o1 , P 为 "foo" o1 有一个叫 "foo" 的自有属性。因此在步骤 2 中, O.[[GetOwnProperty]]("foo") 返回相应的 Property Descriptor (属性描述符),并将其保存在 desc 中。
Property Descriptor是一种规范类型,数据属性描述符把属性的值直接保存在 [[Value]] 字段中,访问器属性描述符把访问器函数保存在字段 [[Get]] 和/或 [Set] 中,在这种情况下,与 "foo" 关联的属性描述符是一个数据属性描述符。
步骤 2 中保存在 desc 中的数据属性描述符不是 undefined ,因此我们在步骤 3 中没有使用 if 分支,接下来执行步骤 4 ,该属性描述符是一个数据属性描述符,因此我们返回其 [[Value]] 字段值为 99 ,步骤 4 到此结束。
Receiver 是什么,它来自哪里?#
Receiver 参数只在步骤 8 的访问器属性中使用,当调用访问器属性的 getter 函数时,它被用作 this 值 传递。
OrdinaryGet 在整个递归过程中传递初始的 Receiver ,没有变化(步骤 3.c),让我们来看看 Receiver 最初来源于哪里!
在搜索调用 [[Get]] 的地方时,我们发现了一个抽象操作 GetValue ,它对引用进行操作。引用是一种规范类型,由一个基础值、一个引用名和一个严格的引用标志组成。以 o2.foo 为例,基础值是对象 o2 ,引用名是字符串 "foo" ,且严格的引用标志是 false ,原因是示例代码没有启用严格模式。
题外话:为什么是引用而非记录? #
题外话:引用并非记录,尽管它听起来可能是,引用包含 3 个组成部分,同样可以被表达为 3 个命名字段。引用并非记录,这是一个历史遗留问题。
回到 GetValue #
让我们看看 GetValue 是如何定义的:
ReturnIfAbrupt(V);- 若
Type(V)为非Reference,则返回V;- 令
base为GetBase(V);- 若
IsUnresolvableReference(V)为true, 抛出ReferenceError异常;- 若
IsPropertyReference(V)为true, 则 a. 若HasPrimitiveBase(V)为true, 则 i. 断言: 在这种情况下,base永远不会是undefined或者null; ii. 设置base为! ToObject(base); b. 返回? base.[[Get]](GetReferencedName(V), GetThisValue(V))。- 否则, a. 断言:
base是一个 Environment Record. b. 返回? base.GetBindingValue(GetReferencedName(V), IsStrictReference(V))
我们例子中的引用是 o2.foo ,是一个属性引用,因此我们选择分支 5 ,我们不会进入 5.a 分支,因为基础对象(o2)不是一个原始值(Number、String、Symbol、BigInt、Boolean、Undefined 或者 Null)。
接着我们在步骤 5.b 中调用 [[Get]] ,而 Receiver 是通过 GetThisValue(V) 传入的,此时,它就是引用的基础值。
- 断言:
IsPropertyReference(V)为true;- 若
IsSuperReference(V)为true,则 a. 返回引用V的thisValue组成部分的值;- 返回
GetBase(V)。
对于 o2.foo ,我们在第 2 步不使用分支,因为它不是一个父类(Super Reference)引用(如 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]] ?[#] (# property-access-get)
规范中哪里说过当访问 o2.foo 这样的属性时, Object 的内部方法 [[Get]] 会被调用?事实上它肯定在某个地方被定义了,切记不要听风就是雨!
我们发现对象内部方法 [[Get]] 是在抽象操作 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;- 返回引用类型值,其基础值为
bv,其引用名称为propertyNameString,其严格引用标志为strict。
也就是说: EvaluatePropertyAccessWithIdentifierKey 构造了一个引用,它使用提供的 baseValue 作为基础,字符串 identifierName 的值作为属性名,strict 作为严格模式标志。
最终,这个引用被传递给 GetValue ,在规范中它有几个地方进行了定义,具体取决于最终如何使用该引用。
MemberExpression 作为一个参数 #
在我们的例子中,我们使用属性访问作为一个参数:
console.log(o2.foo);
在这种情况下,该行为被定义在 ArgumentList 运行时语义的产生式中,它在参数上调用了 GetValue 。
运行时语义:
ArgumentListEvaluation(赋值表达式)
ArgumentList : AssignmentExpression
- 令
ref为AssignmentExpression的求值结果;- 令说
arg为? GetValue(ref);- 返回只包含
arg的列表。
o2.foo 看起来不像是一个 AssignmentExpression ,但它确实是,所以适用于该产生式。
步骤 1 中的 AssignmentExpression 是 o2.foo ,而 o2.foo 的求值结果 ref 就是上面提到的引用,在步骤 2 中我们基于它调用 GetValue 。因此,我们就知道了 Object 的内部方法 [[Get]] 会被调用,而原型链遍历也将触发。
总结 #
在本文中我们看到了规范是如何定义一个语言特性的,如原型查找,跨越了所有不同的层级:触发该特性的语法结构和定义该特性的算法。
转载自:https://juejin.cn/post/7199800513741930553