性能文章>跟着V8引擎读懂 ECMAScript 规范(二)>

跟着V8引擎读懂 ECMAScript 规范(二)原创

6天前
140100

接上篇「跟着V8引擎读懂 ECMAScript 规范(一)」,我们知道属性是在原型链中查找的:如果一个对象没有我们要读取的属性,我们会沿着原型链向上走,直到找到它(或者找到一个不再有原型的对象),如:

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


原型寻址是在哪里定义的?

 

让我们试着找出这个行为是在哪里定义的。对象内部方法列表会是一个好的切入点。

 

对象内部方法列表中有 [[GetOwnProperty]] 和 [[Get]] 两个方法 —— 我们来看不限于自有属性的 [[Get]] 方法。

 

值得注意的是,Property Descriptor 规范类型也有一个名为 [[Get]] 的字段,因此在读 [[Get]] 规范时,我们需要仔细区分这两种独立的用法。

 

[[Get]] 是一个必不可少的内部方法。 普通对象实现基本内部方法的默认行为。 外来对象可以定义自己的内部方法 [[Get]],它偏离默认行为。 本文我们只关注普通对象。

 

委托给 OrdinaryGet 的 [[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. 1. Return ? OrdinaryGet(O, P, Receiver).



之后我们会单独看一下 Receiver 这个参数,它是在调用访问器属性的 getter 函数时用作 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. 1. Assert: IsPropertyKey(P) is true.
  2. 2. Let desc be ? O.[[GetOwnProperty]](P).
  3. 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. 4. If IsDataDescriptor(desc) is true, return desc.[[Value]].
  5. 5. Assert: IsAccessorDescriptor(desc) is true.
  6. 6. Let getter be desc.[[Get]].
  7. 7. If getter is undefined, return undefined.
  8. 8. Return ? Call(getter, Receiver).

 

原型链遍历在步骤 3 中:如果没有找到作为自有属性的那个property,则调用原型的 [[Get]] 方法,该方法再次委托给 OrdinaryGet。 如果仍然找不到该属性,则继续调用其原型的 [[Get]] 方法,该方法再次委托给 OrdinaryGet,依此类推,直到我们找到该属性或最后抵达一个没有原型的对象。

 

让我们看看当我们访问 o2.foo 时这个算法是如何工作的。 首先,我们调用 OrdinaryGet,其中 O 为 o2,P 为“foo”。 O.[[GetOwnProperty]]("foo") 返回 undefined,因为 o2 没有自己的名为“foo”的属性,所以我们在步骤 3 中采用 if 分支。在步骤 3.a 中,我们将 parent 设置为 o2 的原型是 o1。 parent 不为空,因此我们不会在步骤 3.b 中返回。 在步骤 3.c 中,我们使用属性键“foo”调用父级的 [[Get]] 方法,并返回该父对象执行此方法返回的内容。

 

父对象 (o1) 是一个普通对象,因此它的 [[Get]] 方法再次调用 OrdinaryGet,这次 O 是 o1,P 是“foo”。 o1 有一个名为“foo”的自有属性,因此在步骤 2 中,O.[[GetOwnProperty]]("foo") 返回关联的属性描述符,我们将其存储在 desc 中。

 

属性描述符是一种规范类型。 数据属性描述符将属性的值直接存储在 [[Value]] 字段中。 访问器属性描述符将访问器函数存储在字段 [[Get]] 和/或 [[Set]] 中。 在这种情况下,与“foo”关联的属性描述符是数据属性描述符。

 

我们在第2步 desc 中存储的 data Property Descriptor 不是 undefined,所以不会进入第3步的 if 分支。接下来我们执行第4步。Property Descriptor是一个数据Property Descriptor,所以我们返回它的[[Value ]] 字段,99,在步骤 4 中,我们完成了整个原型寻址的过程,得到了最终的结果。

 

OrdinaryGet方法的Receiver参数是什么,它来自哪里?



Receiver 参数仅在步骤 8 中访问器属性的情况下使用。它在调用访问器属性的 getter 函数时作为 this 值传递。

 

OrdinaryGet 在整个递归过程中传递原始 Receiver,保持不变(步骤 3.c)。 让我们找出接收器最初来自哪里!

 

搜索调用 [[Get]] 的地方,我们找到了一个抽象操作 GetValue,它对引用进行操作。 引用是一种规范类型,由基值、引用名称和严格引用标志组成。 在 o2.foo 的情况下,基值是 Object o2,引用的名称是字符串“foo”,严格引用标志为 false(因为示例代码有点草率了)

 

为什么Reference不是Record?

 Reference包含三个组件,它们同样可以表示为三个命名字段。 Reference不是Record(尽管它听起来可能是),此结果可归为历史原因。

 

回到 GetValue,让我们看看 GetValue 是如何定义的:

GetValue ( V )

  1. 1. ReturnIfAbrupt(V).
  2. 2. If Type(V) is not Reference, return V.
  3. 3. Let base be GetBase(V).
  4. 4. If IsUnresolvableReference(V) is true, throw a ReferenceError exception.
  5. 5. 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)).
  6. 6. Else,
    a. Assert: base is an Environment Record.
    b. Return ? base.GetBindingValue(GetReferencedName(V), IsStrictReference(V))

 

我们示例中的引用是 o2.foo,它是一个属性引用。 所以我们会进入分支 5。但我们不会进入 5.a 分支,因为基数 (o2) 不是原始值(数字、字符串、符号、BigInt、布尔值、undefined或null)。

 

然后我们在步骤 5.b 中调用 [[Get]]。 我们传递的 Receiver 是 GetThisValue(V)。 在这种情况下,它只是 Reference 的基础值:

GetThisValue( V )

  1. 1. Assert: IsPropertyReference(V) is true.
  2. 2. If IsSuperReference(V) is true, then
    a. Return the value of the thisValue component of the reference V.
  3. 3. Return GetBase(V).

 

对于 o2.foo,我们不会在第 2 步中使用分支,因为它不是超级引用(例如 super.foo),但我们会在第 3 步中返回引用的基值,即 o2。

 

将所有内容拼凑在一起,我们发现我们将 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 值是我们最初尝试从中获取属性的对象,而不是我们找到它的对象。 在这种情况下,this 值是 o2,而不是 o1。 我们可以通过检查 getter 是返回 o2.x 还是 o1.x 来验证这一点,实际上,它返回了 o2.x。

 

上述代码可以如我们预料运行! 我们能够根据在规范中阅读的内容来预测此代码片段的行为。

 

访问属性为什么会调用 [[Get]]?


Where does the spec say that the Object internal method [[Get]] will get invoked when accessing a property like o2.foo? Surely that has to be defined somewhere. Don’t take my word for it!

规范中在哪儿描述对象内部方法 [[Get]] 将在访问像 o2.foo 这样的属性时被调用?我们发现 Object 内部方法 [[Get]] 是从对 References 进行操作的抽象操作 GetValue 中调用的。 但是从哪里调用 GetValue?

 

成员表达式 MemberExpression 的运行时语义



规范的语法规则定义了语言的语法。 运行时语义定义了句法结构的“含义”(如何在运行时评估它们)。

 

如果您不熟悉上下文无关语法,那么现在可以看看!

We’ll take a deeper look into the grammar rules in a later episode, let’s keep it simple for now! In particular, we can ignore the subscripts (Yield, Await and so on) in the productions for this episode.

我们将在后面更深入地研究语法规则,现在让我们先简单一点! 特别是,我们可以忽略下标部分(Yield、Await 等)。

 

以下产生式描述了 MemberExpression 的样子:

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

 

这里有 7 种用于 MemberExpression 的产生式。 MemberExpression 可以只是 PrimaryExpression。 或者,可以通过将另一个 MemberExpression 和 Expression 拼凑在一起来构造 MemberExpression:MemberExpression [ Expression ],例如 o2['foo']。 或者它可以是 MemberExpression 。 IdentifierName,例如 o2.foo — 这是与我们的示例相关的产生式。

 

生产 MemberExpression 的运行时语义:MemberExpression。 IdentifierName 定义了评估它时要采取的一组步骤:

 

Runtime Semantics: Evaluation for MemberExpression : MemberExpression . IdentifierName

  1. 1. Let baseReference be the result of evaluating MemberExpression.
  2. 2. Let baseValue be ? GetValue(baseReference).
  3. 3. If the code matched by this MemberExpression is strict mode code, let strict be true; else let strict be false.
  4. 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. 1. Assert: identifierName is an IdentifierName
  2. 2. Let bv be ? RequireObjectCoercible(baseValue).
  3. 3. Let propertyNameString be StringValue of identifierName.
  4. 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作为base,identifierName的字符串值作为属性名,strict作为严格模式标志。

 

最终,这个 Reference 被传递给 GetValue。 这在规范中的多个位置进行了定义,具体取决于最终使用参考的方式。

 

成员表达式 MemberExpression 作为参数

 

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

console.log(o2.foo);

 

在这种情况下,行为是在 ArgumentList 产生式的运行时语义中定义的,它在参数上调用 GetValue:

 

Runtime Semantics: ArgumentListEvaluation

ArgumentList : AssignmentExpression

  1. 1. Let ref be the result of evaluating AssignmentExpression.
  2. 2. Let arg be ? GetValue(ref).
  3. 3. Return a List whose sole item is arg.

 

o2.foo 看起来不像一个 AssignmentExpression(但它确实是),所以这个产生式是适用的。 要找出原因,您可以查看此额外内容,但此时并非绝对必要。

 

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

 

小结


本文我们研究了ECMAScript规范如何定义语言特征,在本例中了解到了原型链遍历,跨越所有不同的层:触发特征的句法结构和定义它的算法。

分类:
标签:
请先登录,再评论

暂无回复,快来写下第一个回复吧~

为你推荐

一次 Node.js http 连接无法复用的问题排查
一次压测中阿里云 SLB 的并发连接数被打满了,导致服务之间的 HTTP 调用延迟很大。当时 SLB 的并发连接数情况如下图所示。登录容器终端查看,发现某个前端 Node.js 服务中的单个容器的 E
Chrome V8 javascript engine C++ 高性能垃圾收集器 Oilpan
这篇文章是 Oilpan 系列文章,它将概述 Oilpan 的核心原理及其 C++ API。 在这篇文章中,我们将介绍一些支持的特性,解释它们如何与垃圾收集器的各种子系统交互,并深入探讨在清扫器中并发回收对象。
深入了解 Chrome V8 javascript engine 的垃圾收集引擎
V8 使用分代垃圾收集器,将 Javascript 堆拆分为用于新分配对象的小型年轻代和用于长期存活对象的大型老年代。 由于大多数对象在年轻时死亡,这种分代策略使垃圾收集器能够在较小的年轻代(称为清除)中执行常规的、短时间的垃圾收集,而无需跟踪年老代中的对象。
Chrome V8 javascript engine 的3种垃圾回收算法
从 v6.2 开始,V8 将用于收集年轻代的默认算法切换为并行 Scavenger,本文介绍V8的3种垃圾回收算法:V8 年轻代切尼半空间复制垃圾回收算法、V8 年轻代并行 Mark-Evacuate 垃圾回收算法、V8 年轻代并行 Scavenger 垃圾回收算法
跟着V8引擎读懂 ECMAScript 规范(一)
在本文中,我们通过阅读分析一个简单的方法 —— Object.prototype.hasOwnProperty —— 以及它调用的抽象操作,熟悉了速记 ?和 !与错误处理有关,了解了语言类型、规范类型、内部属性和内部方法。对大家更好地阅读理解 ECMAScript 规范 打下很好的基础。
跟着V8引擎读懂 ECMAScript 规范(二)
本文我们研究了ECMAScript规范如何定义语言特征,在本例中了解到了原型链遍历,跨越所有不同的层:触发特征的句法结构和定义它的算法。
跟着V8引擎读懂 ECMAScript 规范(三)
在本文,我们熟悉了词法语法、句法语法以及用于定义句法语法的简写。 我们研究了在异步函数中禁止使用 await 作为标识符,但在非异步函数中它却可以使用的相关定义。
跟着V8引擎读懂 ECMAScript 规范(四)
在本文中,我们研究了 ECMAScript 规范如何定义覆盖语法(cover grammar),并在我们无法基于有限前瞻(finite lookahead)识别当前句法结构的情况下使用它们。我们研究了将箭头函数参数列表与带括号的表达式区分开来,以及规范如何使用覆盖语法先允许解析看起来模棱两可的结构,