本文为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!
从 ECMAScript 语言规范和浏览器引擎的视角认识 JavaScript
强烈建议按照文章顺序阅读,点击 🔼 上方专栏可以查看所有文章。
前言
大家好,我是早晚会起风。从这篇文章开始,我会正式开始介绍 ECMAScript。
上篇文章中,我们讲过规范内容的大致分类,其中运行时语义是占据篇幅最多的一部分,也是我们疑问最多的地方。在我们深入具体的运行时语义解读之前,首先需要理清一些基础概念。当我们能正确理解这些概念之后,学习起规范内容可以说是事半功倍。
话不多说,我们开始吧。
Runtime semantics 运行时语义
什么是运行时语义?官方定义如下,
Algorithms which specify semantics that must be called at runtime are called runtime semantics
简单来说就是当我们调用 JavaScript API 或者语法时发生的事情,比如 Number(‘23’)
或者一个 for-in
循环的执行过程。它描述的是 JavaScript 在执行时到底干了什么事情。
大部分运行时语义由一系列的算法步骤来表示。
Algorithm steps 算法步骤
算法步骤由多级有序列表组成,列表序号以 数字 -> 小写字母 -> 小写阿拉伯数字 的形式依次出现。形式如下,
1. Top-level step
a. Substep.
b. Substep.
i. Subsubstep.
1. Subsubsubstep
a. Subsubsubsubstep
i. Subsubsubsubsubstep
需要注意的是,算法步骤只是一些伪代码,并不表示特定的技术实现。
我们取规范中的一个例子来直观感受一下,
上面这个例子就是一套算法步骤,它描述了 JavaScript 中 Boolean(value)
这个 API 的执行过程。我们可以很直观的看到当一个值转为布尔值时都经历了哪些步骤。
我们目前并不需要理解这个例子,只需要关注一小点细节。在这个例子中,第一步有一个类似函数调用的过程 ToBoolean(value)
,在规范中这叫做 Abstract Operation。
Abstract Operation 抽象操作
为了避免在规范中重复描述,一些算法步骤被抽象出来形成抽象操作。可以类比为我们写代码时抽象出的可以被复用的函数。抽象操作的引用方式也和我们在 JS 中调用函数类似,例如 OperationName(arg1, arg2)
。
这些抽象操作不是 JavaScript 语言的部分,它们只存在于规范之中,只是为了更好地书写规范。
我们来看看 ToBoolean
这个抽象操作在规范中长啥样,
可以看出,ToBoolean
根据这张表格的定义来决定返回什么值。
抽象操作在规范中被分为四类,
- Type Conversion
- Testing and Comparison Operations
- Operations on Objects
- Operations on Iterator Objects
刚才提到的 ToBoolean
就属于 Type Conversion,也就是类型转换。第二类直译过来叫做测试和比较操作,比如我们经常使用的相等(==
) 和全等(===
)的判断。剩余部分就不一一展开了,我们遇到之后再说。
[[ ]] 是什么
为了后续讲解,我们举另一个例子,
这是我们熟悉的 isPrototypeOf
API,它用于判断一个对象是否是另一个对象的原型。如下,
const grace = {a: 1};
const walk = {b: 2};
Object.setPrototypeOf(grace, walk);
console.log(walk.isPrototypeOf(grace)); // true
在第 3.a. 步,我们可以看到 V.[[GetPrototypeOf]]
这样使用双方括号[[ ]]
包裹起来的名称,这种写法在这里理解为内部方法 Internal Methods。除了内部方法, [[ ]]
会随着使用场景有不同的含义。
1. Internal Methods 内部方法
The actual semantics of objects, in ECMAScript, are specified via algorithms called internal methods. Each object in an ECMAScript engine is associated with a set of internal methods that defines its runtime behaviour.e.
在 ECMAScript 中,对象的实际语义是通过一些叫做 内部方法 的算法来指定的。ECMAScript引擎中的每个对象都与一组定义其运行时行为的内部方法相关联。与算法步骤和抽象操作一样,内部方法不是ECMAScript语言的一部分,只是出于规范描述。
内部方法是多态(polymorphic)的,一个内部方法可能在不同的对象中有不同的定义。在一个普通对象(ordinary object)中, [[GetPrototypeOf]]
这个内部方法定义如下,
除了在这里的定义, [[GetPrototypeOf]]
在 module namespace exotic object 和 Proxy exotic object 这两种对象中也有定义。
规范中定义了两大类对象,ordinary object 和 exotic object。关于这两类对象的区分,我们后边再讲。(挖坑ing…)
2. Internal Slots 内部插槽
内部插槽用于保存对象的内部状态,也用于各种规范算法。在对象初始化时,内部插槽被默认赋值为 undefined。内部插槽并不是对象属性,也不能被继承,这意味着我们不能在 JavaScript 代码中获取到它。
虽然我们在代码中获取不到它,但是我们有时候能看见它。比如在控制台中,我们可以看到一个对象上有一个用于表示对象原型的 [[Prototype]]
。
当我们通过 Object.getPrototypeOf()
来获取对象原型时,实际返回的也是 O.[[Prototype]]
这个内部插槽的值。
内部插槽的值可能是 ECMAScript Language Type
和 ECMASciprt Specification Type
,这里我们简单介绍一下这两种类型,
ECMAScript Language Types(语言类型)。这就是我们在代码中书写的类型,一共有八种—— Undefined、Null、Boolean、String、Symbol、Number、Bigint、Object。
ECMAScript Specification Types(规范类型)。规范类型只存在于规范中,是为了更好地描述语言的底层行为逻辑而存在的,不存在于实际的 js 代码中。规范类型包括 Reference、List、Completion、Property Descriptor、Environment Record、Abstract Closure、Data Block。
3. Record fields
我们刚才提到了 ECMAScript 规范类型,它使用 meta-values 这样的结构来描述,类似 JavaScript 中一个对象的键值对。下图是 Completion Record 的结构(我们稍后就会讲到 Completion Record)
像图片里这样,每一个 Field Name 也会使用 [[ ]]
这种方式来表示。
符号 ? 和 !
我们再次回到刚才的例子,
在步骤 3.a. 中有个 ?
记号。规范中除了它,还经常会出现 !
记号。说来惭愧,我在刚开始阅读规范的时候,理所当然的将这两个符号理解为 JavaScript 中的语义。
实则不然,这两个符号在 ECMAScript 规范中只是作为简写,与 JavaScript 中这两个符号的关系就好比与老婆和老婆饼的关系。我们要理解它,首先就要理解 Complete Record。
Completion Record 完成记录
The Completion Record specification type is used to explain the runtime propagation of values and control flow such as the behaviour of statements (
break
,continue
,return
andthrow
) that perform nonlocal transfers of control.
完成记录规范类型用于解释值和控制流的运行时传递,例如执行非本地控制转移的语句(Break、Continue、Return和Throw)的行为。
完成记录属于我们上边提到的规范类型,它只在规范中出现。
一个完成记录三个字段组成,我们重点看第一个字段 [[Type]]
。类型一共有五种,对于这五种类型规范中也有不同的名称,
- normal completion: 表示完成记录的类型为 normal
- break completion: 表示完成记录的类型为 break
- continue completion: 表示完成记录的类型为 continue
- return completion: 表示完成记录的类型为 return
- throw completion: 表示完成记录的类型为 throw
另外,规范中还定义了两个名称来更方便的标识完成记录的类型,
- abrupt completion: 完成记录的类型不为 normal
- a normal completion contaning some type of value: 表示一个 normal completion 的
[[Value]]
字段中有该类型(some type)的值
normal completion 在规范中可能是使用最多的,因为每个抽象操作都会隐式返回完成记录,即使用 NomalCompletion 将返回值包裹起来。如下,
1. Return true.
⬇ 等价于 ⬇
1. Return NormalCompletion(true).
// NormalCompletion 结构
{
Type: normal,
Value: value,
Target: empty
}
马上我们就会见到 abrupt completion,剩余的一些完成记录类型我们之后如果有遇到再说。
ReturnIfAbrupt
到了揭秘的时候了,规范中的?
其实是 ReturnIfAbrupt Shorthands,即 ReturnIfAbrupt 这个算法的简写。
ReturnIfAbrupt 可以理解为抽象操作在返回值之前的一个校验。
如果完成记录的类型是 abrupt completion(我们刚才提到过,表示 [[type]] 不是 nomal 的 completion),就返回这个完成记录。反之,如果完成记录是一个 normal completion,我们就将 argument 设置为它真正的值 argument.[[Value]]
。
需要注意的是,最后一步是改变 argument 的值,而不是将值返回。怎么理解它呢?我们举一个假例子,
- Let obj be Foo(). (obj is a Completion Record.)
- ReturnIfAbrupt(obj).
- Bar(obj). (If we’re still here,
obj
is the value extracted from the Completion Record.)
第一步抽象操作拿到的 obj
是一个 Completion Record。如果它是一个 normal completion,经过第二步 ReturnIfAbrupt 之后会进入到步骤三。此时, obj
已经变成了 obj.[[Value]]
,也就是它真正的值。
这里隐含的另一个意思是,在算法步骤中我们临时定义的变量可以理解为一个引用值。它就像我们在 JavaScript 的定义的变量一样,可以被重新赋值。
ReturnIfAbrupt shorthands
了解 ReturnIfAbrupt 之后,我们就可以来看它的简写了。很简单,为了书写方便,规范中定义? OperationName()
等同于ReturnIfAbrupt(OperationName())
。
另外,除了问号 ?
,规范中还会出现感叹号 !
,类似这样 Let val be ! OperationName().
。
- Let val be OperationName().
- Assert: val is a normal completion.
- Set val to val.Value.
它用于假设我们的完成记录类型为 normal completion。
了解这两个简写定义后,我们来改写一下上面我们引用的算法步骤,
# 20.1.3.3 Object.prototype.isPrototypeOf ( V )
This method performs the following steps when called:
1. If V is not an Object, return false.
2. Let O be ToObject(this value).
3. Assert: O is a Completion Record.
4. If O is an abrupt completion, return Completion(O).
5. Else, set O to O.[[Value]].
6. Repeat,
a. Set V to V.[[GetPrototypeOf]]().
b. Assert: V is a Completion Record.
c. If V is an abrupt completion, return Completion(V).
d. Else, set V to V.[[Value]].
e. If V is null return false
f. Let temp be SameValue(O, V).
g. Assert: temp is not an abrupt completion.
h. Set temp be temp.[[Value]].
i. If temp is true return true
对比文章开头引用的定义,看看是否和你所想的一样?如果还有疑问,欢迎在评论区留言。
参考资料
最后,欢迎大家点赞评论收藏,有大家的支持才有更新的动力嘛~