跳到主要内容

再读 ES6

阅读需 38 分钟

再一次阅读 es6 书籍,能感受到提升,也发现了一些以前忽视的点或是一些细节,做以记录。适合对 es6 已有一些熟练度。

记录了但未发表,应该是没有完整的整理。

第 1 章 ECMAScript 6 简介

1 1997 ES1,1998 ES2,1999 ES3,2009 ES5,2015 ES6(ES2015),2016 ES7(ES2016),2017 ES8(ES2017)...

2 ecma262

3 ECMAScript proposals 提案。

第 2 章 let 和 const 命令

2.1 基础信息

1 for 循环,设置循环变量部分是一个父作用域,每一次循环,循环体部分都是一个单独的子作用域,循环变量都是一个新的变量。

for (let i = 0; i < 3; i++) {
console.log(i)
}

2 暂时性死区意味着 typeof 不再是一个百分之百安全的操作。如果一个变量根本没有被声明,使用 typeof 反而不会报错,为 undefined。

3 ES5 只可使用 var 和 function 声明变量,ES6 还可使用 let、const、import 和 class。

4 使用 globalThis 来访问全局对象。已通过提案。

2.2 变量提升深究

① 变量提升这个词从何而来

https://developer.mozilla.org/zh-CN/docs/Glossary/Hoisting

② 变量提升的真正定义是什么?如果真的没有”提升“,如何暂时性死区,这个提升是理解上的提升吗。变量提升的定义要明确。

2.3 块级作用域

1 ES5 只有全局作用域和函数作用域,ES6 才有了块级作用域。

2 块级作用域确实是 {}。猜测是根据 {} 来判断块级作用域的。

(function () {
func() // Uncaught TypeError: func is not a function
if (true) {
if (true) {
function func() {
console.log(1)
}
}
}
}())

(function () {
func()
function func() {}
}())

// 边界情况,侧面支持根据 {} 判断块级作用域
if (true)
let a = 1;
console.log(a)
// Uncaught SyntaxError: Lexical declaration cannot appear in a single-statement context

3 建议不要在块级作用域内声明函数。块级作用域内声明函数,声明前不可用,声明后却可越过块级作用域。

(function () {
func() // Uncaught TypeError: func is not a function
if (true) {
if (true) {
function func() {
console.log(1)
}
}
}
func() // 1
}())

第 3 章 变量的解构赋值

3.2 技巧相关

1 p匹配模式,不是变量。

let { p: [x] } = { p: ['Hello'] }

2 记录所有匹配模式对应的值,再统一赋值给匹配模式对应的变量。

let x = 1
let y = 2
;[x, y] = [y , x] // [2, 1]

let x = { a: 1 }
let y = { b: 2 }
;[x, y] = [y , x] // [{ b: 2 }, { a: 1 }]

3 可以写两次去取一条线上的值。一个一个找值。

let { p, p: [x] } = { p: ['Hello'] }

4 不一定是声明新变量来接收值。

const student = {}
({ foo: student.name } = { name: 'evgo' })
student // { name: 'evgo' }

3.2 有关 undefined

1 如果解构不成功,变量的值就等于 undefined。

2 当取值严格(===)等于 undefined ,默认值才生效。

 let [x = 1] = [null]
x // null

3 解构嵌套的对象,子对象所在的父属性不存在,则报错。本质是从 undefined 取属性值。

4 解构赋值的规则是,只要等号右边的值不是对象或数组,就像将其转为对象。而 null 和 undefined 无法转为对象,所以对它们进行解构赋值会报错。

3.3 引擎相关

1 某种数据结构具备 Iterator 接口,就可以采用数组形式解构赋值。

2 避免将大括号写在行首,JavaScript 引擎会将其解释为代码块。

let x
{ x } = { x: 1 } // SynataxError
({ x } = { x: 1 }) // 正确

3 解构赋值对圆括号的规则是,只要有可能导致解构的歧义,就不得使用圆括号。建议不要在匹配模式中放置圆括号。只有赋值语句的非模式部分可以使用圆括号。

let [a] = [1] // 正确
let [(a)] = [1] // 错误,声明语句
[(a)] = [1] // 正确,赋值语句,括号也不属于模式的一部分

第 4 章 字符串的扩展

加强对 Unicode 的支持

1 JavaScript 内部,字符以 UTF-16 的格式存储,一个字符固定为 2 个字节

两个字节存储 2^16 = 65536 个字符,其十六进制为 0xFFFF(16^4 = 2 * 65536)。即 Unicode 码点大于 0xFFFF 就代表超过一个字符。

2 \uxxxx 只限码点在 \u0000~\uFFFF 之间的字符,超过 0xFFFF 的字符必须使用 4 个字节的形式表达,如 \uD842\uDFB7。新增 {} 将码点放入大括号以正确解读,如 \u{20BB7}

'\uD842\uDFB7' // 𠮷
'\u20BB7' // ₻7,理解为 '\u20BB7' + '7'
'\u{20BB7}' // 𠮷

3 需要 4 个字节存储的字符,JavaScript 会认为是两个字符。 新增 codePointAt()以正确处理返回字符的码点。

s = '𠮷'; // \u{20BB7}
// 长度被误判为
s.length // 2
// 十进制,只能分别返回前后2个字节的值
s.charCodeAt(0) // 55362,等同 '\uD842'
s.charCodeAt(1) // 57271,等同 '\uDFB7'
// 十六进制
s.charAt(0) // '\uD842'
s.charAt(1) // '\uDFB7'
// 这也说明 charAt 和 charCodeAt 都是固定读取 2 个字节

s = '𠮷a';
// 长度被误判为
s.length // 3,长度依旧不正确
// 使用 for...of 循环, 正确识别 32 位的 UTF-16 字符
for (let ch of s) {
console.log(ch.codePointAt(0).toString(16)); // 长度正确,输出 20bb7 和 61
}
// 十进制
s.codePointAt(0) // 134071,等同 `\u{20BB7}`
s.codePointAt(1) // 57271,等同 '\uDFB7'
s.codePointAt(2) // 97,等同 "a".charCodeAt(0)
// 十六进制
s.codePointAt(0).toString(16) // 20bb7
s.codePointAt(1).toString(16) // dfb7
s.codePointAt(2).toString(16) // 61

// 这也是测试一个字符是由 2 个字节还是 4 个字节组成的最简单方法。
function is32Bit (c) {
return c.codePointAt(0) > 0xFFFF
}
// 其他获得正确长度的方式
[...'𠮷'].length
Array.from('𠮷').length

Char Code 中文直译是字符码。Code Point 中文直译正好是码点。

4 方法总结

方法含义ES5ES6方法定义位置
返回字符串给定位置的字符charAtat字符串实例对象
返回字符的码点charCodeAtcodePointAt字符串实例对象
返回码点的字符fromCharCodefromCodePointString 对象
支持 Unicode 码点大于 6

ES5 关键字是 charcharCode,以 char 为核心。

ES6 关键字是 省略charcodePoint,以 point 为核心。(at 方法起名为 pointAt 更对称,但会名不副实。)

正好 ES5 是 charCode,ES6 以 code 为承接点,承接为 codePoint

5 Unicode 为表示带有语调或重音的符号,提供了两种方式,一是直接提供带重音符号的字符,如 \u01D1,二是提供合成符号,即原字符和重音符合合成为一个字符,如 \u004F\u030C。这两种方式在视觉和语义上都等价,但 JavaScript 无法识别。

新增 normalize() 将字符的不同表示方法统一为同样的形式,这称为 Unicode 正规化。不过此方法目前不能识别 3 个及以上字符的合成,这种情况下还是只能使用正则表达式,通过 Unicode 编号区间判断。

扩展字符串对象

1 ES6 为字符串添加了遍历器接口,使得字符串可以由 for...of 循环遍历。这个遍历器最大优点是可识别大于 0xFFFF 的码点,正确循环字符长度,传统的 for 循环无法识别。

2 repeat() 参数如果是小数会被向下取整。特殊的是 -1~0 之间的小数等同于 0(-0)所以不报错。NAN 也等同于 0。

3 模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出中,可以用 trim() 消除。默认将字符串转义。

4 模板字符串引用本身:

(new Function('name', 'return ' + '`Hello ${name}`')('evgo'))
(eval.call(null, '(name) => `Hello ${name}`')('evgo'))

5 标签模板:模板字符串紧跟在一个函数名后,该函数将被调用来处理这个模板字符串。注意第一个模板字符串数组参数还有 raw 属性,保存转义后的原字符串,为了方便取得转义之前的原始模板。一个重要应用就是过滤 HTML 字符串。

alert `123` // alert('123')

const name = "evgo"
sayMore `Hello ${name}.\nThis is a sentence.`
// sayMore(['Hello ', '.\nThis is a sentence.'], 'evgo')
// raw: ['Hello ', '.\\nThis is a sentence.']

6 raw() 往往充当模板字符串的处理函数,返回一个反斜线都被转义的字符串,对应于替换变量后的模板字符串。

String.raw({ raw: 'test' }, 0 , 1, 2) // t0e1s2t

第 5 章 正则的扩展

1 ES6 使 match()replace()search()split() 在语言内部全部都调用 RegExp 的实例方法,从而做到所有与正则相关的方法都定义在 RegExp 对象上。

2 之前只支持先行断言、先行否定断言,ES9 现在已支持 后行断言、后行否定断言

3 具名组如果没找到匹配,键名在 groups 内是始终存在的,只是值为 undefined。

第 6 章 数值的扩展

1 二进制(0b0B ),八进制(0o0O),十进制(0d 或者 0D),十六进制(0x0X)。ES5 严格模式已经不允许八进制使用 0 前缀表示了。使用 Number() 转换为十进制数值。

Binary 二进制。Octal 八进制。十进制 Decimal。Hexadecimal 十六进制(hexa 六)。

2 Number.isFinite()Number.isNaN() 只对数值有效,对于非数值一律返回 false。传统 isFinite()isNaN() 先调用 Number() 转换为数值再判断,大多方法都是这样。

3 将 parseInt()parseFloat() 从全局移植到了 Number 对象上,行为完全不变。

4 浮点数计算是不精确的。常量 Number.EPSILON 为浮点数设置一个误差范围,小于则认为计算正确。

0.1 + 0.2 // 0.30000000000000004

5 JavaScript 内部使用相同的存储方式存储整数和浮点数,3 和 3.0 是同一个数。使用 64 位浮点数(国际标准 IEEE 754)表示数值,所以整数精确度只有 53 个二进制位,即准确表示 -2^53 < x < 2^53 内的整数 x,超过这个范围则无法精确表示。所以 JavaScript 不适合科学和金融方面计算。使用 Integer 数据类型只表示整数,没有位数限制,任何位数整数都可以精确表示,必须使用 n 后缀。

 9007199254740992 === 9007199254740993 // 9007199254740993 超过精度存储为 9007199254740992

7 V8 引擎中,指数运算符(**)与 Math.pow() 实现不同,特别大的运算结果两者会有细微差异。

第 7 章 函数的扩展

参数默认值

1 参数变量是默认声明的,不能用 let 或 const 再次声明,但 var 可以。

2 参数默认值是惰性求值。不是传值,每次都重新计算默认值表达式的值。

3 参数默认值为对象的特殊情况。

const m1 = ({ x = 0, y = 0} = {}) => [x, y]
const m2 = ({ x, y } = { x: 0, y: 0 }) => [x, y]

4 传入 undefined 将触发参数等于默认值,null 没有这个效果。将参数默认值设置为 undefined,表明此参数可忽略。

5 函数的 length 属性的含义是该函数预期传入的参数个数。指定了参数默认值以后,值为第一个指定默认值的参数之前的参数个数,rest 参数同理。

6 当设置了参数默认值,在函数进行声明初始化时,会形成一个单独的参数作用域,该作用域在初始化结束后消失。

const x = 1
function func (x, y = x) {
return y
}
func() // undefined
func(2) // 2

let x = 1
function func (y = x) { // 作用域内 x 没有定义,所以从外层作用域取值
let x = 2
return y
}
func() // 1

let x = 1
function func (x, y = function() { x = 2 }) { // 参数默认值是函数,其作用域也在这里面
var x = 3
y() // 函数绑定了 this?
return x
}
func() // 3,两个 x
x // 1

let x = 1
function func (y = function() { x = 2 }) {
var x = 3
y() // 函数绑定了 this?
return x
}
func() // 3
x // 2,函数作用域向上走了

外层作用域 => 参数作用域 => 函数体作用域。

参数只接受赋值,不会从外层作用域去取值。参数默认值如果没有在参数作用域内定义,就会从外层作用域取。

参数默认值如果是函数,则这个函数在参数作用域或者外层作用域,而不在也不会到函数体作用域。this 应该和普通函数有区别。这一点要注意。

箭头函数

1 如果直接返回一个对象,在对象外层加上括号,大括号被解释为代码块。

const func = () => ({ name: 'arrowFunction' })

2 没有自己的 this,所以内部的 this 是外层代码块的 this,也就固定指向定义时所在的对象,而不是执行使用时的对象。不能用作构造函数。

尾调用

1 尾调用:指某个函数最后一步是调用另一个函数。不一定出现在函数尾部,只要是最后一步操作即可。

function f (x) {
return g(x)
}

2 尾调用优化:只保留内层函数的调用帧。这是函数的最后一步,这样每次执行只有一帧调用帧,节省内存。

3 尾递归:尾调用自身。把所有用到的内部变量改写为函数参数。

function factorial (n, total = 1) {
if (n === 1) return total
return factorial(n - 1, n * total)
}

function fibonacci (n, ac1 = 1, ac2 = 1) {
if (n <= 1) return ac2
return fibonacci(n - 1, ac2, ac1 + ac2)
}

4 函数柯里化:将多参数的函数转换为单参数形式。

function currying (x) {
return function (y) {
return x + y
}
}

currying(1)(2) // 3

5 纯粹的函数式编程语言没有循环操作命令,所有循环都用递归实现

6 ES6 第一次明确规定所有 ECMAScript 实现都必须部署尾递归优化。正常模式下函数内部 func.argumentsfunc.caller 变量跟踪函数调用栈,严格模式下会禁用这两个变量,所以尾调归优化只在严格模式生效。

7 正常模式下可以自己实现尾递归优化:采用循环替换递归

// 蹦床函数:返回一个函数然后执行,而不是在函数里面调用函数。但不是真正的尾递归优化。
function trampoline (f) {
while (f && f instanceof Function) {
f = f()
}
return f
}

本质是一个无状态地方只有输入输出,一个有状态地方只有值。优化掉了存储过程的地方。

三者都是可预计的“固定”占用,只是过程在倍速增长,其他两者无增长或可忽略。循环不像递归“默认”记录了过程。循环建立一块地方存储过程,或者递归去掉过程存储,两者就基本等同了。

循环像平房平铺开。递归像高楼层叠起。

其他

1 函数绑定 ::。返回的还是原对象可以链式调用。

2 函数的 name 属性返回该函数的函数名。ES5 中将一个匿名函数赋值给变量,name 属性会返回空字符串,ES6 会返回实际名称。Function 构造函数返回的函数实例 name 属性为 anonymous。bind 返回的函数 name 属性有 bound 前缀。函数是一个 Symbol 值则 name 属性为 Symbol 值的描述。

第 8 章 数组的扩展

扩展运算符

1 ... 将一个数组转为用逗号分隔的参数序列。内部使用 for...of 循环。

const func = (a, b, c) => console.log(a, b, c)
func(...[1, 2, 3]) // 1 2 3

[a, ...rest] = [1, 2, 3]
[a, b, c] = [1, 2, 3]
...rest === b, c
...rest = 2, 3
...[2, 3] = 2, 3
rest // [2, 3]

2 如果后面是一个空数组,则不产生任何效果。

[...[], 1] // 1

3 变通解决函数只能返回一个值问题。

4 正确返回字符串长度。

function length32Bit (c) {
return [...c].length
}

'𠮷'.length // 2
length32Bit('𠮷') // 1

方法

1 Array.from() 将类似数组的对象(本质特征必须有 length 属性,如 DOM 操作返回的 NodeList 集合、arguments 对象)和可遍历(实现了 Iterator 接口,如 Set)对象转为真正的数组。...也是,但这种情况 ... 无法转换。

Array.from({ length: 3 }) // [undefined, undefined, undefined]

// 其他应用
const arr = [1, 2, 3]
Array.from(arr) === arr // false

Array.from({ length: 2 }, () => 'evgo') // ['evgo', 'evgo']
Array.from('𠮷').length

2 Array.of() 将一组值转换为数组。总是返回由参数值组成的数组,而不是像 Array() 返回值和参数个数相关。

Array() // []
Array(3) // [,,,]
Array(1, 2, 3) // [1, 2, 3]

Array.of() // []
Array.of(3) // [3]
Array.of(1, 2, 3) // [1, 2, 3]

3 有些 Array 方法会有 end 参数,代表到该位置之前停止,默认等于数组长度。可以理解为 end 参数代表获取个数。这样细想是有好处的,默认值 length 可直接获取,不需要额外计算,且数组能获取的最大 index 是 length - 1,正好是 length 的前一个,非常统一。

4 includes() 可以发现 NaN,弥补了 indexOf() 的不足(内部使用 === 判断)。

5 数组的空位:指数组的某一个位置没有任何值。空位不是 undefined,一个位置的值是 undefined 依旧是有值的。ES5 对空位处理不一致,ES6 明确空位转为 undefined。建议避免出现空位。

new Array(3) // [empty × 3]

new Array(3).fill() // [undefined, undefined, undefined]
new Array(3).fill('1', 0, 1) // [undefined, empty × 2]
new Array(3).fill('1', 0, 1)[1] // undefined

第 9 章 对象的扩展

基础知识

1 简洁写法中属性名总是字符串。

const obj = { class () {}} // const obj = { 'class': function () {} }

2 属性名表达式如果是一个对象,默认情况自动将对象转为字符串 [object Object]

const key = { a: 1 }
const obj = { [key]: 'value' }
obj // { [object Object]: 'value' }

3 如果对象的方法使用了取值函数(getter)和存值函数(setter),name 属性在该方法属性的描述对象的 getset 属性上。

const obj = {
get foo () {},
set foo (v) {}
}
obj.foo.name // TypeError: Cannot read properties of undefined (reading 'name')
const descriptor = Object.getOwnPropertyDescriptor(obj, 'foo')
descriptor.get.name // 'get foo'
descriptor.set.name // 'set foo'

5 比较算法

名称特性例子
==NaN 不等于 NaN、+0 不等于 -0,自动转换数据类型
===NaN 不等于 NaN、+0 等于 -0
同值相等算法(Same-value equlity)NaN 等于 NaN、+0 不等于 -0,在所有环境中只要值一样就相等Object.is()

Object.assign()

1 将源对象的所有自身可枚举属性浅复制到目标对象。

2 参数如果不是对象,就会先转为对象再返回。目标对象如果无法转为对象会报错。源对象无法转为便跳过,但只有字符串会以数组形式复制到目标对象(字符串的包装对象会产生可枚举属性),其他类型值不会产生效果。

Object.assign({}, 'abc') // { 0: 'a', 1: 'b', 2: 'c'}

Object('abc') // String {'abc'}, { 0: "a", 1: "b", 2: "c", length: 3, [[Prototype]]: String, [[PrimitiveValue]]: "abc" }

3 null 和 undefined 无法转为对象。

4 一旦遇到同名属性,替换而不是添加。

Object.assign({ a: { b: '2', c: '3' } }, { a: { b: '4' }}) // { a: { b: '4' } }

5 把数组视为对象处理。

Object.assign([1, 2, 3], [4, 5]) // [4, 5, 3]

6 无法正确复制 get 属性和 set 属性。因为总是赋值一个属性的值,不会复制背后的赋值方法或取值方法。引入 Object.getOwnPropertyDescriptors() 来配合 Object.defineProperties() 解决。

枚举

1 ES6 一共四种方法会忽略 enumerable: false 属性的方法。

会忽略 enumerable: false 属性的方法可枚举属性范围
for...in自身、继承
Object.keys()自身
JSON.stringify()自身
Object.assign()自身

2 引入 enumerable 最初的目的就是让某些属性可以规避掉 for...in 操作,如 toString() 、length。

3 ES6 规定所有 Class 的原型方法都是不可枚举的。

对象属性

1 对象的每一个属性都具有一个描述对象(Descriptor),用于控制该属性的行为。

2 ES6 一共五种方法可遍历对象属性的方法。都遵循同样的属性遍历次序规则:按照数字排序数值属性、按照生成时间排序字符串属性、按照生成时间排序 Symbol 属性。

可遍历对象属性的方法属性范围枚举范围Symbol 范围
for...in自身、继承可枚举不包含 Symbol 属性
Object.keys(obj)自身可枚举不包含 Symbol 属性
Object.getOwnPropertyNames(obj)自身可枚举、不可枚举不包含 Symbol 属性
Object.getOwnPropertySymbols(obj)自身包含 Symbol 属性
Reflect.ownKeys(obj)自身可枚举、不可枚举包含 Symbol 属性

3 __proto__ 用来读取或设置当前对象的 prototype 对象。目前所有的浏览器都部署了这个属性。ES6 标准明确规定,只有浏览器必须部署这个属性,其他环境不必,而且新代码最好认为这个属性不存在。本质是一个内部属性,只是由于浏览器广泛支持才写入了 ES6。实现上调用的是 Object.prototype.__proto__ 。建议不要调用这个属性,而是使用 Object.getPrototypeOf()Object.setPrototypeOf()Object.create(),如果参数不是对象,会自动转为对象。

Object.getPrototypeOf(1) === Number.prototype // true

4 多种应用

new Map(Object.entries({ a: 1, b: 2 }))
let { x, ...rest } = { x: 1, y: 2, z: 3 } // rest { y:2, z: 3 }。浅复制,且不复制继承属性
{ ...{ a: 1, b: 2 } } // 等同 Object.assign({}, { a: 1 , b: 2 })。只复制了对象实例属性,完整克隆还需要原型属性
{ ...null, ...undefined } // 不报错

第 10 章 Symbol

1 ES5 对象的属性名都是字符串,容易属性名冲突。这就是 ES6 引入 Symbol 的原因。Symbol,表示独一无二的值,通过 Symbol() 生成。

2 是第 7 种 JavaScript 原始数据类型, 前六种是 Undefined、Null、Boolean、String、Number、Object。

3 Symbol 值是一个原始类型的值而不是对象,基本上它是类似于字符串的数据类型。 Symbol() 前不能使用 new 命令 。不能添加属性。不能与其他类型计算,但可显式转为字符串、布尔值。

const sym = Symbol('My symbol')
`your symbol is ${sym}` // TypeError: Cannot convert a Symbol value to a string

`your symbol is ${sym.toString()}` // 'your symbol is Symbol(My symbol)'
`your symbol is ${String(sym)}` // 'your symbol is Symbol(My symbol)'

`your symbol is ${!!sym}` // 'your symbol is true'
`your symbol is ${Boolean(sym)}` // 'your symbol is true'

4 Symbol() 参数只表示对当前 Symbol 值的描述,相同参数的返回值也不同。 Symbol.for()Symbol() 一样生成新的 Symbol,但它的参数被登记在全局环境中供搜索,可以在不同的 iframe 或 service worker 中取到同一个值,不存在才新建一个返回。

Symbol() === Symbol() // false
Symbol('symbol') === Symbol('symbol') // false
Symbol.keyFor(Symbol('symbol')) // undefined

Symbol.for() === Symbol.for() // true
Symbol.for('symbol') === Symbol.for('symbol') // true
Symbol.keyFor(Symbol.for('symbol')) // symbol'

5 对象内部使用 Symbol 值定义属性时,必须放在方括号中。且此属性是公开属性,不是私有属性。不会被常规方法遍历得到,可为对象定义一些公开但只内部的方法。

const size = Symbol('size')
class Collection {
constructor () {
this[size] = 0
}
}

6 魔术字符串是在代码之中多次出现与代码强耦合的某一个具体的字符串或数值。有时等于哪个值并不重要,只要确保不冲突即可。

const shapeType = { triangle: Symbol() }
swicth (shape) { case shapeType.triangles... }

7 Singleton 模式指任何时候调用一个类都返回同一个实例。属性名为字符串易被覆写,为 Symbol.for() 不会无意被覆写,为 Symbol() 外部无法引用无法被覆写(虽然一般不会多次执行导致 key 不同,但用户可手动清除缓存,不完全可靠)。

8 ES6 内置十一个 Symbol 值,指向语言内部使用的方法。

9 数组的默认在 Array.prototype.concat() 时是展开的,类数组对象默认不展开。

第 11 章 Set 和 Map 数据结构

1 Set 加入值时不会发生类型转换,内部判断值是否相同采用同值相等算法。

new Set([1, 2, 3, 3, 3]) // Set {1, 2, 3}
[new Set([1, 2, 3, 3, 3])] // [Set { 1, 2, 3 }]
[...new Set([1, 2, 3, 3, 3])] // [1, 2, 3]
Array.from(new Set([1, 2, 3, 3, 3])) // [1, 2, 3]

2 Set 的遍历顺序就是插入顺序。Set 实例默认可遍历,默认遍历器生成函数就是它的 values 方法。

3 WeakSet 成员只能是对象,成员对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对成员的引用。遍历机制无法保证成员存在,因此 ES6 规定 WeakSet 不可遍历,没有 size 属性。

new WeakSet([[1, 2], [3, 4]]) // WeakSet {[1, 2], [3, 4]},数组的成员

4 JavaScript 对象本质是键值对集合(Hash 结构),但只能字符串为键,字符串-值。所以 ES6 提供了 Map ,键范围不限于字符串,值-值。

5 Map 读取一个未知的键名,返回 undefined。键实际和内存地址绑定。内部判断键名比较采用 === ,但 NaN 采用同值相等算法。

6 Map 的遍历顺序就是插入顺序。Map 实例默认可遍历,默认遍历器生成函数就是它的 entries 方法。

7 Map 根据键名类型是否都是字符串,可选择转为对象 JSON 或数组 JSON。

8 WeakMap 键名只能是对象(除了 null),键名对象都是弱引用,即垃圾回收机制不考虑 WeakMap 对键名对象的引用。但注意键值是正常引用。遍历机制无法保证成员存在,因此 ES6 规定 WeakMap 不可遍历,没有 size 属性。无法清空,没有 clear 方法。

9 监听函数放在 WeakMap 很合适。一旦 DOM 消失,绑定的监听函数也会自动消失。也适合部署私有属性。

第 12 章 Proxy

1 Proxy 修改某些操作的默认行为,属于一种“元编程”,即对编程语言进行编程。

2 has 拦截 HasProperty 而不是 HasOwnProperty ,即不判断自身还是继承属性。且对 for..in 不生效。

3 Object.keys() 会自动过滤不符合的属性,即使 ownKeys() 返回了。

4 Proxy.revocable 的一个使用场景是,目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问。

5 不是目标对象的透明代理,即使不做任何拦截也无法保证与目标对象行为一致。主要原因是代理情况下目标对象内部的 this 指向代理。this 绑定原始对象可解决。

第 13 章 Reflect

1 设计目的:一将 Object 对象上明显属于语言内部的方法(如 Object.defineProperty)放到 Reflect 对象上,现阶段某些方法在两者上同时部署,未来新方法只在 Reflect 上,也就说从 Reflect 上获得语言内部方法。二修改某些 Obejct 方法的返回结果使其更合理。三让 Object 操作都变成函数行为,某些操作是命令式的(如 name in obj)。四 Reflect 对象方法与 Proxy 对象方法一一对应,无论 Proxy 怎么修改默认行为都可以在 Reflect 上获取默认行为。

try { Object.defineProperty(target, propertym attrs) /* success */ } catch (e) { /* fail */ } // 会逐渐被废弃
if (Reflect.defineProperty(target, property, attrs)) { /* success */ } else { /* fail */ }

2 一般来说绑定一个函数的 this 对象,fn.apply() 就可以,但如果函数定义了自己的 apply 方法,就只能写成 Function.prototype.apply.call() 这种形式,使用 Reflect.apply() 简化。

Function.prototype.apply.call(Math.floor, undefined, [1.75]) // 1
Reflect.apply(Math.floor, undefined, [1.75]) // 1

3 基本上 Reflect 方法参数如果不是对象都会报错,而 Object 会转为对象,或返回参数本身,或返回 false。

4 观察者模式(Observe mode)指函数自动观察数据对象的模式,一旦对象有变,函数自动指向。

第 14 章 Promise 对象

1 Promise 是异步编程的一种解决方案,比传统的回调函数和事件更合理强大。最早由社区提出并实现,ES6 将其写进语言标准统一用法,原生提供 Promise 对象,将异步操作以同步操作表达出来。

2 简单来说是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。

3 Promise 构造函数接受一个函数为参数,函数的参数是 resolvereject 函数,由 JavaScript 引擎提供。调用 resolve 和 reject 函数并不会终结 Promise 的参数函数执行。

4 then 方法分别指定 resolved 和 rejected 状态的回调函数,定义在原型对象 Promise.prototype 上的,作用是为 Promise 实例添加状态改变时的回调函数,返回一个新的 Promise 函数。

5 catch 方法指定发生错误时的回调函数,then(null, rejection) 的别名。Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止,但不会冒泡到全局(注意 try catch 同步方法错误)。如果没有使用 catch 方法,Promise 对象抛出的错误就不会有任何反应。无论是否以 catch 方法结尾,只要最后一个方法抛出错误都可能无法捕获,可以增加一个 done 方法或 finally 方法。

6 一般来说,不要在 then 方法内定义 rejected 状态的回调函数,而应总是使用 catch 方法。

const promiseInstance = new Promise(function (resolve, reject) {
// do something
if (/* 异步操作成功 */) {
resolve(value)
} else {
reject(error)
}
})
promiseInstance.then(function (value) {
// success
}, function (error) {
// failure
})

7 如果参数 Promise 实例自身定义了 catch 方法,那么它被 rejected 的时候并不会触发 Promise.all() 的 catch 方法。

8 Promise.resolve() 将现有对象转为 Promise 对象。立即 resolve 的 Promise 对象是在本轮事件循环结束时,而不是下一轮事件循环开始时。Promise.reject() 方法的参数会原封不动地作为 reject 的理由给后续方法的参数,和 Promise.resolve() 不同。

9 不区分函数是同步还是异步,都调用 Promise 来处理,以使用 then 和 catch。但需要解决让同步函数同步执行,让异步函数异步执行,可使用立即执行的匿名函数。

(async () => f())()
(() => new Promise(resolve => resolve(f())))()

第 15 章 Iterator 和 for...of 循环

1 Iterator:需要一种统一的接口机制来为各种不同的数据结构提供统一的访问机制。使得数据结构成员可按某种次序排列。主要供 ES6 新增的 for...of 消费,该循环主动寻找 Iterator 接口,借鉴了其他语言如 C++、Java、C# 和 Python。

2 本质是一个指针对象。只是把接口规格加到了数据结构上,遍历器与所遍历的数据结构实际分开。ES6 规定,默认的 Iterator 接口部署在数据结构的 Symbol.iterator 属性,认为是可遍历(iterable)的。

3 原生部署的数据结构如数组、Set、Map、TypedArray、类数组的(String、函数的 arguments 对象、NodeList 对象)。类数组对象(有键名和 length 属性)可直接引用数组的 Iterator 接口,普通对象部署无效果。对象之所以没有默认部署,是因为对象属性的遍历顺序不确定,需要开发者手动指定,其实并不必要部署,这时对象实际被当作 Map。

4 默认调用 Iterator 接口的场合:解构赋值、... 、yield*、任何接受数组作为参数。

5 Symbol.iterator 方法最简单的实现使用 Generator 函数。return 方法必须返回一个对象,这是 Generator 规格决定的。throw 方法主要配合 Generator 函数使用,一般遍历器对象用不到这个方法。

6 循环方法

循环方法特点
for
forEach无法中途跳出,break 或 return 都失效
for...in循环遍历键名,适用于对象而非数组,以字符串为键名,还会遍历手动添加的其他键甚至原型链,有时任意顺序遍历
for...of循环遍历键值,可中途跳出

第 16 章 Generator 函数的语法

1 Generator 是 ES6 提供的一种异步编程解决方案。语法上可以理解为状态机封装了多个内部状态,执行该函数会返回一个遍历器,ES6 规定这个遍历器是 Generator 函数的实例对象,代表函数内部指针。形式上是一个普通函数,但有星号和 yield(产出)。可以说 Generator 生成了一系列的值,这也是其名称来历(英语含义为生成器)。

就是一个遍历器对象生成函数,在函数里通过 yield 定义遍历可以得到的所有内容,方便的实现 Iterator 接口。Iterator 每次调用 next 就相当于一次 yield,在外部手动记录位置,现在在一个函数内完成,由引擎自动记录位置(但不记录当前 yield 执行的返回值)。

2 ES6 没有规定 function 关键字与函数名之间的星号在哪个位置,一般紧跟 function

function* foo (x, y) { ... }

3 只有调用 next 方法且内部指针执行该语句,才会执行 yield 语句后面的表达式,等同于“惰性求值”语法功能。可以不用 yield 语句,变成单纯的暂缓执行函数,普通函数在为变量赋值时就会执行。

function* gen () { yield 123 + 456 } }

function* f () { console.log('执行了') }
const f = f()
f.next() // 此时才执行

4 则为 return 语句的值。执行完毕 value 属性为 undefined。yield 语句构成的表达式本身没有值,总是等于 undefined。

为什么 yield 不能默认值?既然已经记录了位置,为什么不记录值。传入了值覆盖了默认值就可以?

5 next 方法可以带参数,代表上一次 yield 语句的返回值,这样就可以在函数运行的不同阶段从外部向内部注入不同的值从而调整函数行为。第一次 next 方法是用来启动遍历器对象,所以传值无效。

function* gen (x) {
const y = 2 * (yield (x + 1))
const z = yield (y / 3)
return x + y + z
}

const g = gen()
g.next() // { value: 6, done: false }
g.next() // { value: NaN, done: false }
g.next() // { value: NaN, done: true }

6 for...of 循环自动遍历 Generator 函数生成的 Iterator 对象,此时不需要手动调用 next 方法,且一旦返回对象的 done 为 true 循环就终止,不包含此返回对象。

7 返回的遍历器对象的 throw 方法,可在函数体外抛出错误,在函数体内捕获。不要和全局 throw 命令混淆。throw 方法被捕获以后附带执行一次 next 方法。

8 返回的遍历器对象的 return 方法会推迟到函数体内 finally 执行完再执行。

相关方法都是在返回的遍历器对象上的方法。

9 函数体内调用另一个 Generator 函数,默认情况下没有效果,需要用到 yield* 语句,等同于部署了一个 for...of 循环。可以方便取出嵌套数组的所有成员,等同于 Array.flat()

10 不能和 new 命令一起使用,可以绕。让 Generator 函数返回一个正常的对象实例,既可用 next 方法也可获得正常 this:将一个空对象或原型链绑定 Generator 函数内部的 this。

F.call({})
F.call(F.prototype)

11 协程(coroutine)是一种程序运行方式,可理解为协作的线程或协作的函数。用单线程实现就是一种特殊的子例程,用多线程实现就是一种特殊的线程。可以并行执行、交换执行权的线程或函数。Generator 函数是 ES6 对协程的半实现,只有 Generator 函数的调用者才能将程序的执行权交还给 Generator 函数,而完全协程任何函数都可以让暂停的协程继续执行。完全可以将多个需要互相协作的任务写成 Generator 函数。

异步遍历器

1 目前隐含一个规定,next 方法必须是同步的,只要调用就必须返回。

2 ES9 已支持异步遍历器,设计目的之一是使 Generator 函数处理同步和异步操作可使用同一套接口。同步和异步遍历器最终行为一致,只是异步先返回 Promise 对象作为中介。

async function* map() {}

3 调用遍历器的 next 方法返回 Promise 对象,部署在 Symbol.asyncIterator 属性,只要这个属性有值,无论什么对象都表示对它进行异步遍历。且 next 方法是可以连续调用的,这时 next 方法累积起来,自动按照每一步顺序运行。

4 await 命令将外部操作产生的值输入函数内部,yield 将函数内部的值输出。

async function* perfixLines (asyncIterable) {
for await (const line of asyncIterable) {
yiled '> ' + line
}
}

第 17 章 Generator 函数的异步应用

异步编程方式

1 任务连续完成是同步,任务不连续完成是异步。

2 ES6 之前异步编程大概有四种:回调函数、事件监听、发布/订阅、Promise 对象。

3 回调函数(callback,直译为重新调用)是把后续任务写在一个函数,等到重新执行时直接调用这个函数。因为任务分段执行,一段任务执行完成后,这段任务所在的上下文环境就结束了无法捕获,所以在其之后抛出的错误,只能当作参数传入下一段任务,这也是为什么 Node 约定回调函数的第一个参数必须是错误对象 err(没有错误则该参数为 null)。本身没有问题,问题是多个回调函数嵌套的回调地狱。JavaScript 语言对异步编程的实现就采用这种方式。

4 Promise 对象就是为了解决回调地狱被提出的,不是新的语法,而是新的写法,允许将回调地狱的嵌套改写为链式调用。最大问题是代码冗杂,任务被包裹之后都是 then 堆积,原来语义很不清楚。

是一种写法,所以有不同的实现途径。手写 Promise 就来了。

5 传统的编程语言中早有异步编程的解决方案(其实是多任务的解决方案),其中一种是协程,多个线程互相协作完成异步任务。Generator 函数是协程在 ES6 中的半实现,异步操作需要暂停的地方都用 yield 语句注明。

6 Generator 函数可暂停执行和恢复执行是可封装异步任务的根本原因。而函数体外的数据交换和错误处理机制是支持作为异步编程的完整解决方案,意味着出错的代码与处理错误的代码实现了时间和空间上的分离。对异步编程非常重要。

function* gen () {
const result = yield fetchAsync('https://evgo2017.com')
console.log(result)
}

const g = gen()
const result = g.next()

result.value.then(function (data) {
return data.json()
}).then(function (data) {
g.next(data)
})

核心就是在回调函数里面调用 next()

7 自动执行 Generator 函数的两种方式:Thunk 函数(回调函数交还)、co 模块(Promise then 内交还)。核心机制是当异步操作有了结果,这种机制自动交回控制权。

Thunk 函数

1 在 20 世纪 60 年代就诞生了。那时编程语言刚起步,计算机科学家还在研究如何编写编译器比较好。一个争论焦点是“求值策略”,即函数的参数到底应该在什么时候求值。一种意见是“传值调用”(call by value),即求值再传入,C 语言采用。一种意见是“传名调用”(call by name),即传入再(用到时)求值,Haskell 语言采用。

2 编译器的“传名调用”的实现往往是将参数放到一个临时函数中,再将这个临时函数传入函数体,这个临时函数就是 Thunk 函数。

3 JavaScript 语言是传值调用,它的 Thunk 函数含义有所不同,此 Thunk 函数不替换表达式,而是将多参数函数替换为一个只接受回调函数作为参数的单参数函数。任何函数只要有回调函数就能写成 Thunk 函数形式。

4 以前 Thunk 函数没什么用,但现在可用于 Generator 函数的自动流程管理。yield 后面必须是 Thunk 函数。

co 模块

1 co 将两种自动执行器(回调函数和 Promise 对象)包装为一个模块,yield 后面必须是 Thunk 函数或 Promise 对象(v4.0 以后只支持 Promise 对象)。使用 co 无须编写 Generator 函数的执行器。

function run (gen) {
const g = gen()

function next (data) {
const result = g.next(data)
if (result.done) {
return result.value
}
result.value.then(function (data) {
next(data)
})
}

next()
}

核心的体现

第 18 章 async 函数

1 是 Generator 函数的语法糖。

2 async 函数自带执行器,Generator 函数执行必须靠执行器。async 函数返回 Promise 对象,Generator 函数返回 Iterator 对象。 完全可以看作是由多个异步操作包裹成的一个 Promise 对象。

3 实现原理是将 Generator 函数和自动执行器包装在一个函数内。

async function fn (agrs) {
// ...
}
// 等同于
function fn (args) {
return spawn(function* () {
// ...
})
}

函数内部

1 return 语句的返回值,会成为 then 方法回调函数的参数。函数内部抛出的未被捕获的错误对象,会成为 catch 方法回调函数的参数。

2 async 函数内部的所有异步操作完毕,或 return 或抛出错误,async 函数返回的 Promise 对象定义的的回调才执行。

await 命令

1 如果紧跟的不是 Promise 对象,会被转为一个立即 resolve 的 Promise 对象。

3 多个 await 命令紧跟的异步操作如果不存在继发关系,则最好并发执行。forEach、map 等方法虽然参数是 async 函数,但实际是并发执行的,因为只有 async 函数内部是继发执行,外部不受影响。

let [foo, bar] = await Promise.all([getFoo(), getBar()])
// 或
let fooPromise = getFoo()
let barPromise = getBar()
let foo = await fooPromise()
let bar = await barPromsie()
// 或
docs.forEach(async function (doc) { // map 也是,写为 for 循环就是继发执行了
await db.post(doc)
})

错误处理

1 async 函数内部抛出错误,导致返回的 Promise 对象变为 reject,抛出的错误对象会被 catch 方法捕获。

async function f () {
throw new Error("error in async Function")
return "若无错误则会作为 then 的参数"
}

f().then(
res => console.log(res),
err => console.log(err)
)

// Error: error in async Function

2 如果 await 紧跟的 Primose 对象变为 reject 状态,则 reject 的参数立即被 catch 方法的回调函数捕获,和该 Promise 对象前面加 return 的效果一样。该 Promise 对象内部出错,也等同于被 reject。如果未捕获错误则整个 async 函数中断执行。

async function f () {
await Promise.reject("error in await Promise") // 等同 return await Promise.reject("error in await Promise")
console.log(await Promise.resolve("会执行吗?")) // 不会执行,函数已中断
return await Promise.resolve("若无错误则会作为 then 的参数") // 不会执行,函数已中断
}


f().then(
res => console.log(res),
err => console.log(err)
)

// Error: error in async Promise

3 将错误放在 try catch 内捕获,或者在 await 紧跟的 Promise 对象添加 catch 方法。

async function f () {
try {
throw new Error("error in async function")
// await Promise.reject("error in await Promise") 也可以放在 try catch 内
} catch (err) {
console.log(err)
}
await Promise.reject("error in await Promise").catch(err => {
console.log(err)
})
console.log(await Promise.resolve("会执行吗?")) // 会执行
return "若无错误则会作为 then 的参数" // 或return await Promise.resolve("若无错误则会作为 then 的参数")
}

f().then(
res => console.log(res),
err => console.log(err)
)

// Error: error in async function
// error in await Promise
// 会执行吗?
// 若无错误则会作为 then 的参数

待归类

1.1 Babel

1 presets 字段,设置转码规则,如 babel-preset-latestbabel-preset-stage-0

{
"presets": ["latest", "stage-0"],
"plugins": []
}

2 babel-cli 用于命令行转码。

3 babel-node 提供一个支持 ES6 的 REPL(交互式编程环境)环境。支持 Node 的 REPL 环境的所有功能。不需要单独安装,随 babel-cli 一起安装。

4 babel-register 模块:改写了 require 命令加上了一个钩子,require 之前先转码。只会对 require 命令加载的文件实时转码。

5 babel-core 模块:调用 Babel 的 API 进行转码。

6 babel-polyfill:环境垫片。Babel 默认只转换新的 JavaScript 语法,不转换新的 API。

(Babel应该新开一章)

暂时未加入评论功能,请在对应公众号文章下或 GitHub Issues下留言反馈。