[原文链接](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 argumentV
, the following steps are taken:1.Let
P
be? ToPropertyKey(V)
.
2.LetO
be? ToObject(this value)
.
3.Return? HasOwnProperty(O, P)
.
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 argumentsO
andP
whereO
is the object andP
is the property key. This abstract operation performs the following steps:1.Assert:
Type(O)
isObject
.
2.Assert:IsPropertyKey(P)
istrue
.
3.Letdesc
be? O.[[GetOwnProperty]](P)
.
4.Ifdesc
isundefined
, 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 ofO
is 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
argument
is abrupt, returnargument
2.Setargument
toargument.[[Value]]
.
也就是说,我们会检查完成记录;如果是 abrupt 立即返回。否则,从完成记录中提取 [[value]]。
ReturnIfAbrupt 看起来像函数调用,但其实不然。它的行为更像是类 C 语言中的宏。
ReturnIfAbrupt 可以这样使用:
1.Let
obj
beFoo()
. (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
beFoo()
.
2.Assert:val
is not an abrupt completion.
3.Setval
toval.[[Value]]
.
基于这些,我们就可以重写 Object.prototype.hasOwnProperty
Object.prototype.hasOwnProperty(V)
- Let
P
beToPropertyKey(V)
.- If
P
is an abrupt completion, returnP
- Set
P
toP.[[Value]]
- Let
O
beToObject(this value)
.- If
O
is an abrupt completion, returnO
- Set
O
toO.[[Value]]
- Let
temp
beHasOwnProperty(O, P)
.- If
temp
is an abrupt completion, returntemp
- Let
temp
betemp.[[Value]]
- Return
NormalCompletion(temp)
然后是 HasOwnProperty
HasOwnProperty(O, P)
- Assert:
Type(O)
isObject
.- Assert:
IsPropertyKey(P)
istrue
.- Let
desc
beO.[[GetOwnProperty]](P)
.- If
desc
is an abrupt completion, returndesc
- Set
desc
todesc.[[Value]]
- If
desc
isundefined
, returnNormalCompletion(false)
.- Return
NormalCompletion(true)
.
我们还可以重写不带感叹号的内部方法 [[GetOwnProperty]]
O.[[GetOwnProperty]]
- Let
temp
beOrdinaryGetOwnProperty(O, P)
.- Assert:
temp
is not an abrupt completion.- Let
temp
betemp.[[Value]]
.- Return
NormalCompletion(temp)
.
这里我们假设 temp 是一个新的临时变量,它不会与其他任何东西发生冲突。
另外,我们还用到了一点:当 return 语句返回完成记录以外的东西时,它会隐式地包裹在 NormalCompletion 中。
Return ? Foo()
规范中用到了 Return ? Foo() - 为什么会有问号?
Return ? Foo() 可以展开:
1.Let
temp
beFoo()
.
2.Iftemp
is an abrupt completion, returntemp
.
3.Settemp
totemp.[[Value]]
.
4.ReturnNormalCompletion(temp)
.
和 Return Foo() 是一样的,对于 abrupt 和 normal completion 其行为是一致的。
Return ? Foo() 仅是出于编辑的考虑,为了更明确地表达 Foo
返回一个完成记录。
断言(Asserts)
规范中的 Asserts 用来断言算法的不变条件。添加断言是为了更清晰地表述规范,但不需要对实现添加任何需求 - 实现不需要检查它们。
继续探索(Moving on)
某种抽象操作会委托给其他抽象操作(见下图),但基于本文,我们应该能够弄清楚它们的作用。我们将遇到属性描述符,它只是另一种规范类型。
总结
我们阅读了一个简单的方法 - Object.prototype.hasOwnProperty 和它调用的抽象操作。我们了解了与错误处理相关的简写符号 ? 和 !。我们遇到了语言类型、规范类型、内部插槽和内部方法。