├── 读懂 ECMAScript 规格
│ ├── 概述
│ ├── 术语
│ │ └─ 抽象操作
│ │ └─ 所谓“抽象操作”(abstract operations)就是引擎的一些内部方法,外部不能调用。规格定义了一系列的抽象操作,规定了它们的行为,留给各种引擎自己去实现。
│ │ └─ 举例来说,`Boolean(value)`的算法,第一步是这样的。
│ │ └─ 这里的`ToBoolean`就是一个抽象操作,是引擎内部求出布尔值的算法。
│ │ └─ 许多函数的算法都会多次用到同样的步骤,所以 ES6 规格将它们抽出来,定义成“抽象操作”,方便描述。
│ │ └─ Record 和 field
│ │ └─ ES6 规格将键值对(key-value map)的数据结构称为 Record,其中的每一组键值对称为 field。这就是说,一个 Record 由多个 field 组成,而每个 field 都包含一个键名(key)和一个键值(value)。
│ │ └─ [[Notation]]
│ │ └─ ES6 规格大量使用`[[Notation]]`这种书写法,比如`[[Value]]`、`[[Writable]]`、`[[Get]]`、`[[Set]]`等等。它用来指代 field 的键名。
│ │ └─ 举例来说,`obj`是一个 Record,它有一个`Prototype`属性。ES6 规格不会写`obj.Prototype`,而是写`obj.[[Prototype]]`。一般来说,使用`[[Notation]]`这种书写法的属性,都是对象的内部属性。
│ │ └─ F.[[Call]](V, argumentsList)
│ │ └─ 上面代码中,`F`是一个函数对象,`[[Call]]`是它的内部方法,`F.[[call]]()`表示运行该函数,`V`表示`[[Call]]`运行时`this`的值,`argumentsList`则是调用时传入函数的参数。
│ │ └─ Completion Record
│ │ └─ 每一个语句都会返回一个 Completion Record,表示运行结果。每个 Completion Record 有一个`[[Type]]`属性,表示运行结果的类型。
│ │ └─ - normal
│ │ └─ - return
│ │ └─ - throw
│ │ └─ - break
│ │ └─ - continue
│ │ └─ 如果`[[Type]]`的值是`normal`,就称为 normal completion,表示运行正常。其他的值,都称为 abrupt completion。其中,开发者只需要关注`[[Type]]`为`throw`的情况,即运行出错;`break`、`continue`、`return`这三个值都只出现在特定场景,可以不用考虑。
│ ├── 抽象操作的标准流程
│ │ └─ 抽象操作的运行流程,一般是下面这样。
│ │ └─ > 1. Let `result` be `AbstractOp()`.
│ │ └─ > 1. If `result` is an abrupt completion, return `result`.
│ │ └─ > 1. Set `result` to `result.[[Value]]`.
│ │ └─ > 1. return `result`.
│ │ └─ 上面的第一步调用了抽象操作`AbstractOp()`,得到`result`,这是一个 Completion Record。第二步,如果`result`属于 abrupt completion,就直接返回。如果此处没有返回,表示`result`属于 normal completion。第三步,将`result`的值设置为`resultCompletionRecord.[[Value]]`。第四步,返回`result`。
│ │ └─ ES6 规格将这个标准流程,使用简写的方式表达。
│ │ └─ > 1. Let `result` be `AbstractOp()`.
│ │ └─ > 1. `ReturnIfAbrupt(result)`.
│ │ └─ > 1. return `result`.
│ │ └─ 1. Let `result` be `? AbstractOp()`.
│ │ └─ 上面流程的`?`,就代表`AbstractOp()`可能会报错。一旦报错,就返回错误,否则取出值。
│ │ └─ 1. Let `result` be `! AbstractOp()`.
│ │ └─ 上面流程的`!`,代表`AbstractOp()`不会报错,返回的一定是 normal completion,总是可以取出值。
│ ├── 相等运算符
│ │ └─ 规格对每一种语法行为的描述,都分成两部分:先是总体的行为描述,然后是实现的算法细节。相等运算符的总体描述,只有一句话。
│ │ > 1. 如果`x`不是正常值(比如抛出一个错误),中断执行。
│ │ > 1. 如果`y`不是正常值,中断执行。
│ │ > 1. 如果`Type(x)`与`Type(y)`相同,执行严格相等运算`x === y`。
│ │ > 1. 如果`x`是`null`,`y`是`undefined`,返回`true`。
│ │ > 1. 如果`x`是`undefined`,`y`是`null`,返回`true`。
│ │ > 1. 如果`Type(x)`是数值,`Type(y)`是字符串,返回`x == ToNumber(y)`的结果。
│ │ > 1. 如果`Type(x)`是字符串,`Type(y)`是数值,返回`ToNumber(x) == y`的结果。
│ │ > 1. 如果`Type(x)`是布尔值,返回`ToNumber(x) == y`的结果。
│ │ > 1. 如果`Type(y)`是布尔值,返回`x == ToNumber(y)`的结果。
│ │ > 1. 如果`Type(x)`是字符串或数值或`Symbol`值,`Type(y)`是对象,返回`x == ToPrimitive(y)`的结果。
│ │ > 1. 如果`Type(x)`是对象,`Type(y)`是字符串或数值或`Symbol`值,返回`ToPrimitive(x) == y`的结果。
│ │ > 1. 返回`false`。
│ │ └─ 数组的空位
│ │ └─ 数组的成员是`undefined`或空位,到底有什么不同?
│ │ └─ "数组成员可以省略。只要逗号前面没有任何表达式,数组的`length`属性就会加 1,并且相应增加其后成员的位置索引。被省略的成员不会被定义。如果被省略的成员是数组最后一个成员,则不会导致数组`length`属性增加。”
│ │ └─ 上面的规格说得很清楚,数组的空位会反映在`length`属性,也就是说空位有自己的位置,但是这个位置的值是未定义,即这个值是不存在的。如果一定要读取,结果就是`undefined`(因为`undefined`在 JavaScript 语言中表示不存在)。
│ │ └─ 这就解释了为什么`in`运算符、数组的`hasOwnProperty`方法、`Object.keys`方法,都取不到空位的属性名。因为这个属性名根本就不存在,规格里面没说要为空位分配属性名(位置索引),只说要为下一个元素的位置索引加 1。
│ ├── 数组的 map 方法
│ │ └─ `arr`是一个全是空位的数组,`map`方法遍历成员时,发现是空位,就直接跳过,不会进入回调函数。因此,回调函数里面的`console.log`语句根本不会执行,整个`map`方法返回一个全是空位的新数组。