[原文链接](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
hasOwnPropertymethod is called with argumentV, the following steps are taken:1.Let
Pbe? ToPropertyKey(V).
2.LetObe? ToObject(this value).
3.Return? HasOwnProperty(O, P).
The abstract operation
HasOwnPropertyis 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 argumentsOandPwhereOis the object andPis the property key. This abstract operation performs the following steps:1.Assert:
Type(O)isObject.
2.Assert:IsPropertyKey(P)istrue.
3.Letdescbe? O.[[GetOwnProperty]](P).
4.Ifdescisundefined, returnfalse.
5.Returntrue.
到这,我们会有很多疑惑。"abstract operation" 是什么?[[ ]] 里面是什么?函数的前面为什么有问号?assert 是什么意思?
接下来,我们将一一揭晓。
语言类型和规范类型(Language types and specification types)
先从简单的开始,规范中用到的 undefined, true, 和 false 我们已经在 JavaScript 中非常熟悉了,它们都是 语言值(language values),这些值是规范中定义的语言类型所包含的值。
规范也会在内部使用语言值,例如,某个内部数据类型可能包含了一个字段(field),它的值可能是 true 和 false。相比之下,JavaScript 引擎通常不会在内部使用语言值。例如,如果 JavaScript 引擎是用 C++ 编写的,它通常会使用 C++ 的 true 和 false (而不是 JavaScript 内部的 true 和 false)。
除了语言类型,规范中还会使用 规范类型(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]]:
When the
[[GetOwnProperty]]internal method ofOis called with property keyP, the following steps are taken:1.Return
! OrdinaryGetOwnProperty(O, P).
(我们会在接下来的章节中了解到感叹号的具体含义)
OrdinaryGetOwnProperty 不是一个内部方法,因为它不与任何对象关联;相反,它操作的对象是作为一个参数传递进来的。
OrdinaryGetOwnProperty 被称为"普通",因为它操作普通对象。ECMAScript 对象可以是普通的,也可以是奇异(exotic)的。普通对象必须具有一组所谓基础内部方法的默认行为。如果一个对象偏离了默认的行为,那么它就是奇异对象。
最具代表性的奇异对象是 Array,因为它的 length 属性没有表现出默认行为:设置 length 属性可以删除 Array 中的元素。
基础内部方法的完整列表 在这里。
完成记录(Completion records)
为了搞清楚问号和感叹号,我们需要查阅规范 Completion Records。
完成记录是一种规范类型(仅为规范而定义)。JavaScript 引擎不需要有相应的内部数据类型。
完成记录是一类'记录'(record) - 具有一组固定命名字段的数据类型。完成记录有以下三个字段:
| Name | Description |
|---|---|
[[Type]] | One of: normal, break, continue, return, 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
argumentis abrupt, returnargument
2.Setargumenttoargument.[[Value]].
也就是说,我们会检查完成记录;如果是 abrupt 立即返回。否则,从完成记录中提取 [[value]]。
ReturnIfAbrupt 看起来像函数调用,但其实不然。它的行为更像是类 C 语言中的宏。
ReturnIfAbrupt 可以这样使用:
1.Let
objbeFoo(). (objis a Completion Record.)
2.ReturnIfAbrupt(obj).
3.Bar(obj). (If we’re still here,objis the value extracted from the Completion Record.)
现在问号就开始派上用场了:? Foo() 等价于 ReturnIfAbrupt(Foo()),这种简写是很有用的。我们不需要每次都显式地编写错误处理代码。
类似的,! Foo() 等价于:
1.Let
valbeFoo().
2.Assert:valis not an abrupt completion.
3.Setvaltoval.[[Value]].
基于这些,我们就可以重写 Object.prototype.hasOwnProperty
Object.prototype.hasOwnProperty(V)
- Let
PbeToPropertyKey(V).- If
Pis an abrupt completion, returnP- Set
PtoP.[[Value]]- Let
ObeToObject(this value).- If
Ois an abrupt completion, returnO- Set
OtoO.[[Value]]- Let
tempbeHasOwnProperty(O, P).- If
tempis an abrupt completion, returntemp- Let
tempbetemp.[[Value]]- Return
NormalCompletion(temp)
然后是 HasOwnProperty
HasOwnProperty(O, P)
- Assert:
Type(O)isObject.- Assert:
IsPropertyKey(P)istrue.- Let
descbeO.[[GetOwnProperty]](P).- If
descis an abrupt completion, returndesc- Set
desctodesc.[[Value]]- If
descisundefined, returnNormalCompletion(false).- Return
NormalCompletion(true).
我们还可以重写不带感叹号的内部方法 [[GetOwnProperty]]
O.[[GetOwnProperty]]
- Let
tempbeOrdinaryGetOwnProperty(O, P).- Assert:
tempis not an abrupt completion.- Let
tempbetemp.[[Value]].- Return
NormalCompletion(temp).
这里我们假设 temp 是一个新的临时变量,它不会与其他任何东西发生冲突。
另外,我们还用到了一点:当 return 语句返回完成记录以外的东西时,它会隐式地包裹在 NormalCompletion 中。
Return ? Foo()
规范中用到了 Return ? Foo() - 为什么会有问号?
Return ? Foo() 可以展开:
1.Let
tempbeFoo().
2.Iftempis an abrupt completion, returntemp.
3.Settemptotemp.[[Value]].
4.ReturnNormalCompletion(temp).
和 Return Foo() 是一样的,对于 abrupt 和 normal completion 其行为是一致的。
Return ? Foo() 仅是出于编辑的考虑,为了更明确地表达 Foo 返回一个完成记录。
断言(Asserts)
规范中的 Asserts 用来断言算法的不变条件。添加断言是为了更清晰地表述规范,但不需要对实现添加任何需求 - 实现不需要检查它们。
继续探索(Moving on)
某种抽象操作会委托给其他抽象操作(见下图),但基于本文,我们应该能够弄清楚它们的作用。我们将遇到属性描述符,它只是另一种规范类型。
总结
我们阅读了一个简单的方法 - Object.prototype.hasOwnProperty 和它调用的抽象操作。我们了解了与错误处理相关的简写符号 ? 和 !。我们遇到了语言类型、规范类型、内部插槽和内部方法。