如何阅读 ECMAScript 规范 - 第一部分

390 阅读7分钟

[原文链接](Understanding the ECMAScript spec, part 1 · V8)

注:文中所列算法可能会与最新的规范有所差别,但影响不大。

前言

尽管你已经入门 JavaScript,阅读规范仍然是一个极大的挑战。至少当我第一次阅读时是这么觉得的。

让我们从一个简单的例子开始,粗略地体验下阅读规范的过程。下面的代码验证了 Object.prototype.hasOwnProperty 的使用。

const o = { foo: 1 };
o.hasOwnProperty('foo');  // true
o.hasOwnProperty('bar');  // false

在这个示例中,o 并没有 hasOwnProperty 属性,通过原型链的查找,我们发现它存在于o的原型 - Object.prototype 上。

为了描述 Object.prototype.hasOwnProperty 是如何工作的,规范采用了类似于伪代码的描述方式:

Object.prototype.hasOwnProperty

When the hasOwnProperty method is called with argument V, the following steps are taken:

1.Let P be ? ToPropertyKey(V).
2.Let O be ? ToObject(this value).
3.Return ? HasOwnProperty(O, P).

HasOwnProperty

The abstract operation HasOwnProperty is used to determine whether an object has an own property with the specified property key. A Boolean value is returned. The operation is called with arguments O and P where O is the object and P is the property key. This abstract operation performs the following steps:

1.Assert: Type(O) is Object.
2.Assert: IsPropertyKey(P) is true.
3.Let desc be ? O.[[GetOwnProperty]](P).
4.If desc is undefined, return false.
5.Return true.

到这,我们会有很多疑惑。"abstract operation" 是什么?[[ ]] 里面是什么?函数的前面为什么有问号?assert 是什么意思?

接下来,我们将一一揭晓。

语言类型和规范类型(Language types and specification types)

先从简单的开始,规范中用到的 undefinedtrue, 和 false 我们已经在 JavaScript 中非常熟悉了,它们都是 语言值(language values),这些值是规范中定义的语言类型所包含的值。

规范也会在内部使用语言值,例如,某个内部数据类型可能包含了一个字段(field),它的值可能是 truefalse。相比之下,JavaScript 引擎通常不会在内部使用语言值。例如,如果 JavaScript 引擎是用 C++ 编写的,它通常会使用 C++ 的 truefalse (而不是 JavaScript 内部的 truefalse)。

除了语言类型,规范中还会使用 规范类型(specification type),它只会出现在规范中,而不会存在于 JavaScript 语言中。JavaScript 引擎不需要(但可以自行抉择)实现它们。在本文中,我们将了解规范类型 Record (以及它的子类型 Completion Record)。

抽象操作(Abstract Operation)

抽象操作 是 ECMAScript 规范中定义的函数,定义它们的目的是为了简洁地编写规范。JavaScript 引擎不必在引擎中实现它们作为单独的函数。它们不能直接从 JavaScript 调用。

内部插槽和内部方法(Internal slots and internal methods)

内部插槽和内部方法 的名称被包裹在 [[ ]] 内。

内部插槽是 JavaScript 对象或规范类型的数据成员(data members),用来存储对象的状态。内部方法是 JavaScript 对象的成员函数(member functions)。

比如,每个 JavaScript 对象都有一个内部插槽 [[Prototype]] 和 一个内部方法 [[GetOwnProperty]]。
JavaScript 不能访问内部插槽和方法,你没办法获取 o.[[Prototype]] 或是调用 o.[[GetOwnProperty]]()。JavaScript引擎可以实现它们供自己内部使用,但这不是必须的。

有时,内部方法会委托给具有类似名称的抽象操作,比如普通对象的 [[GetOwnProperty]]:

[[GetOwnProperty]](P)

When the [[GetOwnProperty]] internal method of O is called with property key P, the following steps are taken:

1.Return ! OrdinaryGetOwnProperty(O, P).

(我们会在接下来的章节中了解到感叹号的具体含义)

OrdinaryGetOwnProperty 不是一个内部方法,因为它不与任何对象关联;相反,它操作的对象是作为一个参数传递进来的。

OrdinaryGetOwnProperty 被称为"普通",因为它操作普通对象。ECMAScript 对象可以是普通的,也可以是奇异(exotic)的。普通对象必须具有一组所谓基础内部方法的默认行为。如果一个对象偏离了默认的行为,那么它就是奇异对象。

最具代表性的奇异对象是 Array,因为它的 length 属性没有表现出默认行为:设置 length 属性可以删除 Array 中的元素。

基础内部方法的完整列表 在这里

完成记录(Completion records)

为了搞清楚问号和感叹号,我们需要查阅规范 Completion Records

完成记录是一种规范类型(仅为规范而定义)。JavaScript 引擎不需要有相应的内部数据类型。

完成记录是一类'记录'(record) - 具有一组固定命名字段的数据类型。完成记录有以下三个字段:

NameDescription
[[Type]]One of: normalbreakcontinuereturn, or throw. All other types except normal are abrupt completions.
[[Value]]The value that was produced when the completion occurred, for example, the return value of a function or the exception (if one is thrown).
[[Target]]Used for directed control transfers (not relevant for this blog post).

每个抽象操作都会隐式返回一个完成记录,即使该操作看起来会返回一个简单类型,例如 Boolean,该返回值会被隐式地包裹到 [[Type]] 为 normal 的完成记录中(可查询规范 Implicit Completion Values)。

注1: 规范在这方面表现的并不完全一致;有一些辅助函数返回裸值并直接使用,而不会从完成记录中提取值。这在具体的上下文中一般都比较清晰。

注2:规范正在考虑使完成记录的处理更加明确。

如果一个算法抛出异常,则会返回一个 [[Type]] 为 throw 的完成记录,其 [[value]] 是一个异常对象。我们暂且忽略 [[Type]] 为 break,continue 和 return 的情况。

ReturnIfAbrupt(argument) 有下面两个步骤:

1.If argument is abrupt, return argument
2.Set argument to argument.[[Value]].

也就是说,我们会检查完成记录;如果是 abrupt 立即返回。否则,从完成记录中提取 [[value]]。

ReturnIfAbrupt 看起来像函数调用,但其实不然。它的行为更像是类 C 语言中的宏。

ReturnIfAbrupt 可以这样使用:

1.Let obj be Foo(). (obj is a Completion Record.)
2.ReturnIfAbrupt(obj).
3.Bar(obj). (If we’re still here, obj is the value extracted from the Completion Record.)

现在问号就开始派上用场了:? Foo() 等价于 ReturnIfAbrupt(Foo()),这种简写是很有用的。我们不需要每次都显式地编写错误处理代码。

类似的,! Foo() 等价于:

1.Let val be Foo().
2.Assert: val is not an abrupt completion.
3.Set val to val.[[Value]].

基于这些,我们就可以重写 Object.prototype.hasOwnProperty

Object.prototype.hasOwnProperty(V)

  1. Let P be ToPropertyKey(V).
  2. If P is an abrupt completion, return P
  3. Set P to P.[[Value]]
  4. Let O be ToObject(this value).
  5. If O is an abrupt completion, return O
  6. Set O to O.[[Value]]
  7. Let temp be HasOwnProperty(O, P).
  8. If temp is an abrupt completion, return temp
  9. Let temp be temp.[[Value]]
  10. Return NormalCompletion(temp)

然后是 HasOwnProperty

HasOwnProperty(O, P)

  1. Assert: Type(O) is Object.
  2. Assert: IsPropertyKey(P) is true.
  3. Let desc be O.[[GetOwnProperty]](P).
  4. If desc is an abrupt completion, return desc
  5. Set desc to desc.[[Value]]
  6. If desc is undefined, return NormalCompletion(false).
  7. Return NormalCompletion(true).

我们还可以重写不带感叹号的内部方法 [[GetOwnProperty]]

O.[[GetOwnProperty]]

  1. Let temp be OrdinaryGetOwnProperty(O, P).
  2. Assert: temp is not an abrupt completion.
  3. Let temp be temp.[[Value]].
  4. Return NormalCompletion(temp).

这里我们假设 temp 是一个新的临时变量,它不会与其他任何东西发生冲突。

另外,我们还用到了一点:当 return 语句返回完成记录以外的东西时,它会隐式地包裹在 NormalCompletion 中。

Return ? Foo()

规范中用到了 Return ? Foo() - 为什么会有问号?

Return ? Foo() 可以展开:

1.Let temp be Foo().
2.If temp is an abrupt completion, return temp.
3.Set temp to temp.[[Value]].
4.Return NormalCompletion(temp).

和 Return Foo() 是一样的,对于 abrupt 和 normal completion 其行为是一致的。

Return ? Foo() 仅是出于编辑的考虑,为了更明确地表达 Foo 返回一个完成记录。

断言(Asserts)

规范中的 Asserts 用来断言算法的不变条件。添加断言是为了更清晰地表述规范,但不需要对实现添加任何需求 - 实现不需要检查它们。

继续探索(Moving on)

某种抽象操作会委托给其他抽象操作(见下图),但基于本文,我们应该能够弄清楚它们的作用。我们将遇到属性描述符,它只是另一种规范类型。

call-graph.svg

总结

我们阅读了一个简单的方法 - Object.prototype.hasOwnProperty 和它调用的抽象操作。我们了解了与错误处理相关的简写符号 ? 和 !。我们遇到了语言类型、规范类型、内部插槽和内部方法。