《阮一峰 ECMAScript 6 (ES6) 标准入门教程 第三版》 - 书栈网 · BookStack
1. 声明变量方法
var function let const import class
前面两个是es5的,后面是es6的,let const 的好处是块变量:只在声明所在的块级作用域内有效;
const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。
2. 取顶层对象
JavaScript 语言存在一个顶层对象,它提供全局环境(即全局作用域),所有代码都是在这个环境中运行。但是,顶层对象在各种实现里面是不统一的。
- 浏览器里面,顶层对象是
window,但 Node 和 Web Worker 没有window。 - 浏览器和 Web Worker 里面,
self也指向顶层对象,但是 Node 没有self。 - Node 里面,顶层对象是
global,但其他环境都不支持。
同一段代码为了能够在各种环境,都能取到顶层对象,现在一般是使用this变量,但是有局限性。
- 全局环境中,
this会返回顶层对象。但是,Node 模块和 ES6 模块中,this返回的是当前模块。 - 函数里面的
this,如果函数不是作为对象的方法运行,而是单纯作为函数运行,this会指向顶层对象。但是,严格模式下,这时this会返回undefined。 - 不管是严格模式,还是普通模式,
new Function('return this')(),总是会返回全局对象。但是,如果浏览器用了 CSP(Content Security Policy,内容安全策略),那么eval、new Function这些方法都可能无法使用。
综上所述,很难找到一种方法,可以在所有情况下,都取到顶层对象。下面是两种勉强可以使用的方法。
-
// 方法一 -
(typeof window !== 'undefined' -
? window -
: (typeof process === 'object' &&
`typeof require === 'function' &&`
`typeof global === 'object')`
`? global`
`: this);`
-
// 方法二 -
var getGlobal = function () { -
if (typeof self !== 'undefined') { return self; } -
if (typeof window !== 'undefined') { return window; } -
if (typeof global !== 'undefined') { return global; } -
throw new Error('unable to locate global object'); -
};
es6采用的方式是globalThis,都可以从它拿到顶层对象
3. 变量解构
数组字符串解构按顺序
let [a, b, c] = [1, 2, 3];const [a, b, c, d, e] = 'hello';
对象解构按对象名
let { bar, foo } = { foo: 'aaa', bar: 'bbb' };let { foo: foo, bar: bar } = { foo: 'aaa', bar: 'bbb' };
数值和布尔值解构
-
let {toString: s} = 123; -
s === Number.prototype.toString // true -
let {toString: s} = true; -
s === Boolean.prototype.toString // true -
let { prop: x } = undefined; // TypeError -
let { prop: y } = null; // TypeError
函数解构
[1, undefined, 3].map((x = 'yes') => x);// [ 1, 'yes', 3 ]
结构默认值:
-
var {x = 3} = {}; -
x // 3 -
var { message: msg = 'Something went wrong' } = {}; -
msg // "Something went wrong"
这里要注意:
// 错误的写法let x;{x} = {x: 1};// SyntaxError: syntax error
上面代码的写法会报错,因为 JavaScript 引擎会将{x}理解成一个代码块,从而发生语法错误。
// 正确的写法let x;({x} = {x: 1});
不能使用圆括号
(1)变量声明语句
(2)函数参数
(3)赋值语句的模式
4. 字符串拓展
模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。
- ``$('#result').append(```
There are <b>${basket.count}</b> itemsin your basket, <em>${basket.onSale}</em>are on sale!- ```);``
模板字符串前后的空格和换行可以使用trim()来处理
` this is a template string `.trim()
"标签模板"的一个重要应用,就是过滤 HTML 字符串,防止用户输入恶意内容。
-
let a = 5; -
let b = 10; -
tag`Hello ${ a + b } world ${ a * b }`; -
// 等同于 -
tag(['Hello ', ' world ', ''], 15, 50);
5. 字符串新增方法
ES6 提供了String.fromCodePoint()方法,可以识别大于0xFFFF的字符,弥补了String.fromCharCode()方法的不足。在作用上,正好与下面的codePointAt()方法相反。
String.fromCodePoint(0x20BB7)// "𠮷"String.fromCodePoint(0x78, 0x1f680, 0x79) === 'x\uD83D\uDE80y'// true
ES6 还为原生的 String 对象,提供了一个raw()方法。该方法返回一个斜杠都被转义(即斜杠前面再加一个斜杠)的字符串,往往用于模板字符串的处理方法。
- ``// `foo${1 + 2}bar```
// 等同于String.raw({ raw: ['foo', 'bar'] }, 1 + 2) // "foo3bar"
语调符号和重音符号。为了表示它们,Unicode 提供了两种方法。一种是直接提供带重音符号的字符,比如Ǒ(\u01D1)。另一种是提供合成符号(combining character),即原字符与重音符号的合成,两个字符合成一个字符,比如O(\u004F)和ˇ(\u030C)合成Ǒ(\u004F\u030C)。
-
'\u01D1'==='\u004F\u030C' //false -
'\u01D1'.length // 1 -
'\u004F\u030C'.length // 2 -
'\u01D1'.normalize() === '\u004F\u030C'.normalize() -
// true
传统上,JavaScript 只有indexOf方法,可以用来确定一个字符串是否包含在另一个字符串中。ES6 又提供了三种新方法。和后面两个和python一致
- includes():返回布尔值,表示是否找到了参数字符串。
- startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
- endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。
-
let s = 'Hello world!'; -
s.startsWith('world', 6) // true -
s.endsWith('Hello', 5) // true -
s.includes('Hello', 6) // false
repeat方法返回一个新字符串,表示将原字符串重复n次。小数会向下取整,负数或者Infinity会报错
'x'.repeat(3) // "xxx"'hello'.repeat(2) // "hellohello"'na'.repeat(0) // ""
ES2017 引入了字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。padStart()用于头部补全,padEnd()用于尾部补全。
-
'x'.padStart(5, 'ab') // 'ababx' -
'x'.padStart(4, 'ab') // 'abax' -
'x'.padEnd(5, 'ab') // 'xabab' -
'x'.padEnd(4, 'ab') // 'xaba'
ES2019 对字符串实例新增了trimStart()和trimEnd()这两个方法。它们的行为与trim()一致,trimStart()消除字符串头部的空格,trimEnd()消除尾部的空格。它们返回的都是新字符串,不会修改原始字符串。浏览器还部署了额外的两个方法,trimLeft()是trimStart()的别名,trimRight()是trimEnd()的别名。
-
const s = ' abc '; -
s.trim() // "abc" -
s.trimStart() // "abc " -
s.trimEnd() // " abc"
matchAll()方法返回一个正则表达式在当前字符串的所有匹配,详见《正则的扩展》的一章。
6. 正则的拓展
感觉和现在的python差不多,这里要注意是exec进行调用
-
const RE_OPT_A = /^(?<as>a+)?$/; -
const matchObj = RE_OPT_A.exec(''); -
matchObj.groups.as // undefined -
'as' in matchObj.groups // true
字符串对象共有 4 个方法,可以使用正则表达式:match()、replace()、search()和split()。
ES6 将这 4 个方法,在语言内部全部调用RegExp的实例方法,从而做到所有与正则相关的方法,全都定义在RegExp对象上。
String.prototype.match调用RegExp.prototype[Symbol.match]String.prototype.replace调用RegExp.prototype[Symbol.replace]String.prototype.search调用RegExp.prototype[Symbol.search]String.prototype.split调用RegExp.prototype[Symbol.split]
7. 数值的拓展
ES6 在Number对象上,新提供了Number.isFinite()和Number.isNaN()两个方法。它们与传统的全局方法isFinite()和isNaN()的区别在于,传统方法先调用Number()将非数值的值转为数值,再进行判断,而这两个新方法只对数值有效,Number.isFinite()对于非数值一律返回false, Number.isNaN()只有对于NaN才返回true,非NaN一律返回false。
ES6 将全局方法parseInt()和parseFloat(),移植到Number对象上面,行为完全保持不变。其功能是解析string转化为int或者float
Number.parseInt('12.34') // 12Number.parseFloat('123.45#') // 123.45
Number.isInteger()用来判断一个数值是否为整数。
Number.isInteger(5E-324) // falseNumber.isInteger(5E-325) // true
如果一个数值的绝对值小于Number.MIN_VALUE(5E-324)会被自动转为 0,上面代码中,5E-325由于值太小,会被自动转为0,因此返回true。
ES6 在Number对象上面,新增一个极小的常量Number.EPSILON。根据规格,它表示 1 与大于 1 的最小浮点数之间的差。Number.EPSILON实际上是 JavaScript 能够表示的最小精度。误差如果小于这个值,就可以认为已经没有意义了,即不存在误差了。
0.1 + 0.2 === 0.3 // false
JavaScript 能够准确表示的整数范围在-2^53到2^53之间(不含两个端点),超过这个范围,无法精确表示这个值。ES6 引入了Number.MAX_SAFE_INTEGER和Number.MIN_SAFE_INTEGER这两个常量,用来表示这个范围的上下限。Number.isSafeInteger()则是用来判断一个整数是否落在这个范围之内。
JavaScript 所有数字都保存成 64 位浮点数,这给数值的表示带来了两大限制。一是数值的精度只能到 53 个二进制位(相当于 16 个十进制位),大于这个范围的整数,JavaScript 是无法精确表示的,这使得 JavaScript 不适合进行科学和金融方面的精确计算。二是大于或等于2的1024次方的数值,JavaScript 无法表示,会返回Infinity。
-
// 超过 53 个二进制位的数值,无法保持精度 -
Math.pow(2, 53) === Math.pow(2, 53) + 1 // true -
// 超过 2 的 1024 次方的数值,无法表示 -
Math.pow(2, 1024) // Infinity
8. 函数的拓展
如果有默认值的参数都不是尾参数。这时,无法只省略该参数,而不省略它后面的参数,除非显式输入undefined。ES2017 允许函数的最后一个参数有尾逗号(trailing comma)。
-
// 例一 -
function f(x = 1, y) { -
return [x, y]; -
} -
f() // [1, undefined] -
f(2) // [2, undefined] -
f(, 1) // 报错 -
f(undefined, 1) // [1, 1] -
// 例二 -
function f(x, y = 5, z) { -
return [x, y, z]; -
} -
f() // [undefined, 5, undefined] -
f(1) // [1, 5, undefined] -
f(1, ,2) // 报错 -
f(1, undefined, 2) // [1, 5, 2]
指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length属性将失真。
(function (a) {}).length // 1(function (a = 5) {}).length // 0(function (a, b, c = 5) {}).length // 2
如果设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数了。
(function (a = 0, b, c) {}).length // 0(function (a, b = 1, c) {}).length // 1
ES6 引入 rest 参数(形式为...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。下面是一个 rest 参数代替arguments变量的例子。
-
// arguments变量的写法 -
function sortNumbers() { -
return Array.prototype.slice.call(arguments).sort(); -
} -
// rest参数的写法 -
const sortNumbers = (...numbers) => numbers.sort();
arguments对象不是数组,而是一个类似数组的对象。所以为了使用数组的方法,必须使用Array.prototype.slice.call先将其转为数组。rest 参数就不存在这个问题,它就是一个真正的数组,数组特有的方法都可以使用。下面是一个利用 rest 参数改写数组push方法的例子。
注意,rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。
-
function push(array, ...items) { -
items.forEach(function(item) { -
array.push(item); -
console.log(item); -
}); -
} -
var a = []; -
push(a, 1, 2, 3)
ES2016 做了一点修改,规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。这样规定的原因是,函数内部的严格模式,同时适用于函数体和函数参数。但是,函数执行的时候,先执行函数参数,然后再执行函数体。这样就有一个不合理的地方,只有从函数体之中,才能知道参数是否应该以严格模式执行,但是参数却应该先于函数体执行。
function doSomething(a, b) {'use strict';// code}
函数的name属性,返回该函数的函数名。
const bar = function baz() {};bar.name // "baz"
箭头函数
ES6 允许使用"箭头"(=>)定义函数。
-
var f = v => v; -
// 等同于 -
var f = function (v) { -
return v; -
};
如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。
-
var f = () => 5; -
// 等同于 -
var f = function () { return 5 }; -
var sum = (num1, num2) => num1 + num2; -
// 等同于 -
var sum = function(num1, num2) { -
return num1 + num2; -
};
ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。递归本质上是一种循环操作。纯粹的函数式编程语言没有循环操作命令,所有的循环都用递归实现,这就是为什么尾递归对这些语言极其重要。对于其他支持"尾调用优化"的语言(比如 Lua,ES6),只需要知道循环可以用递归代替,而一旦使用递归,就最好使用尾递归。
9. 数组的拓展
扩展运算符(spread)是三个点(...)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。
-
console.log(...[1, 2, 3]) -
// 1 2 3 -
console.log(1, ...[2, 3, 4], 5) -
// 1 2 3 4 5
复制数组
const a1 = [1, 2];// 写法一const a2 = [...a1];// 写法二const [...a2] = a1;
合并数组
-
const arr1 = ['a', 'b']; -
const arr2 = ['c']; -
const arr3 = ['d', 'e']; -
// ES5 的合并数组 -
arr1.concat(arr2, arr3); -
// [ 'a', 'b', 'c', 'd', 'e' ] -
// ES6 的合并数组 -
[...arr1, ...arr2, ...arr3] -
// [ 'a', 'b', 'c', 'd', 'e' ]
两种转化数组的方式,array.from() array.of()
Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。
Array.of方法用于将一组值,转换为数组。
数组实例的find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined。
[1, 4, -5, 10].find((n) => n < 0)// -5
上面代码找出数组中第一个小于 0 的成员。
[1, 5, 10, 15].find(function(value, index, arr) {return value > 9;}) // 10
数组实例的findIndex方法的用法与find方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1。
[1, 5, 10, 15].findIndex(function(value, index, arr) {return value > 9;}) // 2
fill方法使用给定值,填充一个数组。
-
['a', 'b', 'c'].fill(7) -
// [7, 7, 7] -
new Array(3).fill(7) -
// [7, 7, 7]
数组也是键值对组成的['a', 'b'] 相当于 {0: 'a', 1: 'b'},其键就是序号,其值是本身;
entries(),keys()和values()------用于遍历数组。它们都返回一个遍历器对象(详见《Iterator》一章),可以用for...of循环进行遍历,唯一的区别是keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历。
数组的成员有时还是数组,Array.prototype.flat()用于将嵌套的数组"拉平",变成一维的数组。该方法返回一个新数组,对原数据没有影响。
-
[1, 2, [3, [4, 5]]].flat() -
// [1, 2, 3, [4, 5]] -
[1, 2, [3, [4, 5]]].flat(2) -
// [1, 2, 3, 4, 5]
flatMap()方法对原数组的每个成员执行一个函数(相当于执行Array.prototype.map()),然后对返回值组成的数组执行flat()方法。该方法返回一个新数组,不改变原数组。
// 相当于 [[2, 4], [3, 6], [4, 8]].flat()[2, 3, 4].flatMap((x) => [x, x * 2])// [2, 4, 3, 6, 4, 8]
10. 对象的扩展
变量foo直接写在大括号里面。这时,属性名就是变量名, 属性值就是变量值。下面是另一个例子。
-
let birth = '2000/01/01'; -
const Person = { -
name: '张三', -
//等同于birth: birth -
birth, -
// 等同于hello: function ()... -
hello() { console.log('我的名字是', this.name); } -
};
ES6 允许字面量定义对象时,用方法二(表达式)作为对象的属性名,即把表达式放在方括号内。
-
let lastWord = 'last word'; -
const a = { -
'first word': 'hello', -
[lastWord]: 'world' -
}; -
a['first word'] // "hello" -
a[lastWord] // "world" -
a['last word'] // "world"
注意,属性名表达式如果是一个对象{},默认情况下会自动将对象转为字符串[object Object],这一点要特别小心。
我们知道,this关键字总是指向函数所在的当前对象,ES6 又新增了另一个类似的关键字super,指向当前对象的原型对象。
三种super的用法都会报错,因为对于 JavaScript 引擎来说,这里的super都没有用在对象的方法之中。第一种写法是super用在属性里面,第二种和第三种写法是super用在一个函数里面,然后赋值给foo属性。目前,只有对象方法的简写法可以让 JavaScript 引擎确认,定义的是对象的方法。
-
// 报错 -
const obj = { -
foo: super.foo -
} -
// 报错 -
const obj = { -
foo: () => super.foo -
} -
// 报错 -
const obj = { -
foo: function () { -
return super.foo -
} -
}
对象的拓展运算符
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };let { x, ...y, ...z } = someObject; // 句法错误let { ...x, y, z } = someObject; // 句法错误let { x, ...y, ...z } = someObject; // 句法错误
注意,解构赋值的拷贝是浅拷贝,即如果一个键的值是复合类型的值(数组、对象、函数)、那么解构赋值拷贝的是这个值的引用,而不是这个值的副本。
let obj = { a: { b: 1 } };let { ...x } = obj;obj.a.b = 2;x.a.b // 2
ES2020 引入了"链判断运算符"(optional chaining operator)?.
-
const firstName = (message -
&& message.body -
&& message.body.user -
&& message.body.user.firstName) || 'default'; -
const firstName = message?.body?.user?.firstName || 'default'; -
const fooValue = myForm.querySelector('input[name=foo]')?.value
相等运算符(==)和严格相等运算符(===)ES6 提出"Same-value equality"(同值相等)算法,用来解决这个问题。Object.is就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。
-
Object.is('foo', 'foo') -
// true -
Object.is({}, {}) -
// false -
+0 === -0 //true -
NaN === NaN // false -
Object.is(+0, -0) // false -
Object.is(NaN, NaN) // true
Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。
Object.assign方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。
-
const obj1 = {a: {b: 1}}; -
const obj2 = Object.assign({}, obj1); -
obj1.a.b = 2; -
obj2.a.b // 2
对于这种嵌套的对象,一旦遇到同名属性,Object.assign的处理方法是替换,而不是添加。
const target = { a: { b: 'c', d: 'e' } }const source = { a: { b: 'hello' } }Object.assign(target, source)// { a: { b: 'hello' } }
Object.assign可以用来处理数组,但是会把数组视为对象。
Object.assign只能进行值的复制,如果要复制的值是一个取值函数,那么将求值后再复制。
-
const source = { -
get foo() { return 1 } -
}; -
const target = {}; -
Object.assign(target, source) -
// { foo: 1 }
ES2017 引入了Object.getOwnPropertyDescriptors()方法,返回指定对象所有自身属性(非继承属性)的描述对象。
-
const obj = { -
foo: 123, -
get bar() { return 'abc' } -
}; -
Object.getOwnPropertyDescriptors(obj) -
// { foo: -
// { value: 123, -
// writable: true, -
// enumerable: true, -
// configurable: true }, -
// bar: -
// { get: [Function: get bar], -
// set: undefined, -
// enumerable: true, -
// configurable: true } }
__proto__属性(前后各两个下划线),用来读取或设置当前对象的原型对象(prototype)。目前,所有浏览器(包括 IE11)都部署了这个属性。但是不推荐使用,实现上,__proto__调用的是Object.prototype.__proto__,具体实现如下。
Object.setPrototypeOf方法的作用与__proto__相同,用来设置一个对象的原型对象(prototype),返回参数对象本身。它是 ES6 正式推荐的设置原型对象的方法。
Object.getPrototypeOf()该方法与Object.setPrototypeOf方法配套,用于读取一个对象的原型对象。
Object.keys(),Object.values(),Object.entries() 作为遍历一个对象的补充手段,供for...of循环使用。
Object.fromEntries()方法是Object.entries()的逆操作,用于将一个键值对数组转为对象。
12. Symbol
ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。
Symbol 作为属性名,遍历对象的时候,该属性不会出现在for...in、for...of循环中,也不会被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回。
但是,它也不是私有属性,有一个Object.getOwnPropertySymbols()方法,可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。
-
const obj = {}; -
const foo = Symbol('foo'); -
obj[foo] = 'bar'; -
for (let i in obj) { -
console.log(i); // 无输出 -
} -
Object.getOwnPropertyNames(obj) // [] -
Object.getOwnPropertySymbols(obj) // [Symbol(foo)]
13. Set 和 Map 数据结构
Set 结构的实例有以下属性。
Set.prototype.constructor:构造函数,默认就是Set函数。Set.prototype.size:返回Set实例的成员总数。
Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。下面先介绍四个操作方法。
Set.prototype.add(value):添加某个值,返回 Set 结构本身。Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功。Set.prototype.has(value):返回一个布尔值,表示该值是否为Set的成员。Set.prototype.clear():清除所有成员,没有返回值。
keys方法、values方法、entries方法返回的都是遍历器对象(详见《Iterator 对象》一章)。由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致。
WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。首先,WeakSet 的成员只能是对象,而不能是其他类型的值。其次,WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。
Map是JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。
Map 结构的实例有以下属性和操作方法。
size属性返回 Map 结构的成员总数。
set方法设置键名key对应的键值为value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键。
set方法返回的是当前的Map对象,因此可以采用链式写法。
get方法读取key对应的键值,如果找不到key,返回undefined。
has方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。
delete方法删除某个键,返回true。如果删除失败,返回false。
clear方法清除所有成员,没有返回值。
Map.prototype.keys():返回键名的遍历器。Map.prototype.values():返回键值的遍历器。Map.prototype.entries():返回所有成员的遍历器。Map.prototype.forEach():遍历 Map 的所有成员。
(1)Map 转为数组
前面已经提过,Map 转为数组最方便的方法,就是使用扩展运算符(...)。
const myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);[...myMap]// [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]
(2)数组 转为 Map
将数组传入 Map 构造函数,就可以转为 Map。
new Map([[true, 7],[{foo: 3}, ['abc']]])// Map {// true => 7,// Object {foo: 3} => ['abc']// }
(3)Map 转为对象
如果所有 Map 的键都是字符串,它可以无损地转为对象。
-
function strMapToObj(strMap) { -
let obj = Object.create(null); -
for (let [k,v] of strMap) {
`obj[k] = v;`
-
} -
return obj; -
} -
const myMap = new Map() -
.set('yes', true) -
.set('no', false); -
strMapToObj(myMap) -
// { yes: true, no: false }
如果有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名。
(4)对象转为 Map
对象转为 Map 可以通过Object.entries()。
let obj = {"a":1, "b":2};let map = new Map(Object.entries(obj));
此外,也可以自己实现一个转换函数。
-
function objToStrMap(obj) { -
let strMap = new Map(); -
for (let k of Object.keys(obj)) {
`strMap.set(k, obj[k]);`
-
} -
return strMap; -
} -
objToStrMap({yes: true, no: false}) -
// Map {"yes" => true, "no" => false}
(5)Map 转为 JSON
Map 转为 JSON 要区分两种情况。一种情况是,Map 的键名都是字符串,这时可以选择转为对象 JSON。
-
function strMapToJson(strMap) { -
return JSON.stringify(strMapToObj(strMap)); -
} -
let myMap = new Map().set('yes', true).set('no', false); -
strMapToJson(myMap) -
// '{"yes":true,"no":false}'
另一种情况是,Map 的键名有非字符串,这时可以选择转为数组 JSON。
-
function mapToArrayJson(map) { -
return JSON.stringify([...map]); -
} -
let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']); -
mapToArrayJson(myMap) -
// '[[true,7],[{"foo":3},["abc"]]]'
(6)JSON 转为 Map
JSON 转为 Map,正常情况下,所有键名都是字符串。
-
function jsonToStrMap(jsonStr) { -
return objToStrMap(JSON.parse(jsonStr)); -
} -
jsonToStrMap('{"yes": true, "no": false}') -
// Map {'yes' => true, 'no' => false}
但是,有一种特殊情况,整个 JSON 就是一个数组,且每个数组成员本身,又是一个有两个成员的数组。这时,它可以一一对应地转为 Map。这往往是 Map 转为数组 JSON 的逆操作。
-
function jsonToMap(jsonStr) { -
return new Map(JSON.parse(jsonStr)); -
} -
jsonToMap('[[true,7],[{"foo":3},["abc"]]]') -
// Map {true => 7, Object {foo: 3} => ['abc']}
WeakMap结构与Map结构类似,也是用于生成键值对的集合。WeakMap与Map的区别有两点。首先,WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。其次,WeakMap的键名所指向的对象,不计入垃圾回收机制。
和weakset一样
14. Proxy
Proxy 可以理解成,在目标对象之前架设一层"拦截",外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来"代理"某些操作,可以译为"代理器"。
下面是 Proxy 支持的拦截操作一览,一共 13 种。
- get(target, propKey, receiver) :拦截对象属性的读取,比如
proxy.foo和proxy['foo']。 - set(target, propKey, value, receiver) :拦截对象属性的设置,比如
proxy.foo = v或proxy['foo'] = v,返回一个布尔值。 - has(target, propKey) :拦截
propKey in proxy的操作,返回一个布尔值。 - deleteProperty(target, propKey) :拦截
delete proxy[propKey]的操作,返回一个布尔值。 - ownKeys(target) :拦截
Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。 - getOwnPropertyDescriptor(target, propKey) :拦截
Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。 - defineProperty(target, propKey, propDesc) :拦截
Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。 - preventExtensions(target) :拦截
Object.preventExtensions(proxy),返回一个布尔值。 - getPrototypeOf(target) :拦截
Object.getPrototypeOf(proxy),返回一个对象。 - isExtensible(target) :拦截
Object.isExtensible(proxy),返回一个布尔值。 - setPrototypeOf(target, proto) :拦截
Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。 - apply(target, object, args) :拦截 Proxy 实例作为函数调用的操作,比如
proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。 - construct(target, args) :拦截 Proxy 实例作为构造函数调用的操作,比如
new proxy(...args)。
-
var person = { -
name: "张三" -
}; -
var proxy = new Proxy(person, { -
get: function(target, propKey) { -
if (propKey in target) { -
return target[propKey]; -
} else { -
throw new ReferenceError("Prop name \"" + propKey + "\" does not exist."); -
} -
} -
}); -
proxy.name // "张三" -
proxy.age // 抛出一个错误
Proxy.revocable()方法返回一个可取消的 Proxy 实例。
Proxy.revocable()方法返回一个对象,该对象的proxy属性是Proxy实例,revoke属性是一个函数,可以取消Proxy实例。上面代码中,当执行revoke函数之后,再访问Proxy实例,就会抛出一个错误。
Proxy.revocable()的一个使用场景是,目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问。
15. Reflect
Reflect对象与Proxy对象一样,也是 ES6 为了操作对象而提供的新 API。Reflect对象的设计目的有这样几个。
(1) 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。现阶段,某些方法同时在Object和Reflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法。
(2) 修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false。
(3) 让Object操作都变成函数行为。某些Object操作是命令式,比如name in obj和delete obj[name],而Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)让它们变成了函数行为。
(4)Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。
Reflect对象一共有 13 个静态方法。
- Reflect.apply(target, thisArg, args)
- Reflect.construct(target, args)
- Reflect.get(target, name, receiver)
- Reflect.set(target, name, value, receiver)
- Reflect.defineProperty(target, name, desc)
- Reflect.deleteProperty(target, name)
- Reflect.has(target, name)
- Reflect.ownKeys(target)
- Reflect.isExtensible(target)
- Reflect.preventExtensions(target)
- Reflect.getOwnPropertyDescriptor(target, name)
- Reflect.getPrototypeOf(target)
- Reflect.setPrototypeOf(target, prototype)
上面这些方法的作用,大部分与Object对象的同名方法的作用都是相同的,而且它与Proxy对象的方法是一一对应的。下面是对它们的解释。
16. Promise
Promise对象有以下两个特点。
(1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是"承诺",表示其他手段无法改变。
(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
我们可以将图片的加载写成一个Promise,一旦加载完成,Promise的状态就发生变化。
const preloadImage = function (path) {return new Promise(function (resolve, reject) {const image = new Image();image.onload = resolve;image.onerror = reject;image.src = path;});};
同时要注意:
-
setTimeout(function () { -
console.log('three'); -
}, 0); -
Promise.resolve().then(function () { -
console.log('two'); -
}); -
console.log('one'); -
// one -
// two -
// three
上面代码中,setTimeout(fn, 0)在下一轮"事件循环"开始时执行,Promise.resolve()在本轮"事件循环"结束时执行,console.log('one')则是立即执行,因此最先输出。
实际开发中,经常遇到一种情况:不知道或者不想区分,函数f是同步函数还是异步操作,但是想用 Promise 来处理它。因为这样就可以不管f是否包含异步操作,都用then方法指定下一步流程,用catch方法处理f抛出的错误。一般就会采用下面的写法。
由于Promise.try为所有操作提供了统一的处理机制,所以如果想用then方法管理流程,最好都用Promise.try包装一下,其中一点就是可以更好地管理异常。
Promise.try(() => database.users.get({id: userId})).then(...).catch(...)
基本操作
ES6 规定,Promise对象是一个构造函数,用来生成Promise实例。
下面代码创造了一个Promise实例。
-
const promise = new Promise(function(resolve, reject) { -
// ... some code -
if (/* 异步操作成功 */){
`resolve(value);`
-
} else {
`reject(error);`
-
} -
});
Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。
resolve函数的作用是,将Promise对象的状态从"未完成"变为"成功"(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从"未完成"变为"失败"(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。
Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。
promise.then(function(value) {// success}, function(error) {// failure});
then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。其中,第二个函数是可选的,不一定要提供。这两个函数都接受Promise对象传出的值作为参数。
getJSON("/post/1.json").then(post => getJSON(post.commentURL)).then(comments => console.log("resolved: ", comments),err => console.log("rejected: ", err));
Promise.prototype.catch()方法是.then(null, rejection)或.then(undefined, rejection)的别名,用于指定发生错误时的回调函数。
getJSON('/posts.json').then(function(posts) {// ...}).catch(function(error) {// 处理 getJSON 和 前一个回调函数运行时发生的错误console.log('发生错误!', error);});
上面代码中,getJSON()方法返回一个 Promise 对象,如果该对象状态变为resolved,则会调用then()方法指定的回调函数;如果异步操作抛出错误,状态就会变为rejected,就会调用catch()方法指定的回调函数,处理这个错误。另外,then()方法指定的回调函数,如果运行中抛出错误,也会被catch()方法捕获。
-
p.then((val) => console.log('fulfilled:', val)) -
.catch((err) => console.log('rejected', err)); -
// 等同于 -
p.then((val) => console.log('fulfilled:', val)) -
.then(null, (err) => console.log("rejected:", err));
finally()方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。
promise.then(result => {···}).catch(error => {···}).finally(() => {···});
上面代码中,不管promise最后的状态,在执行完then或catch指定的回调函数以后,都会执行finally方法指定的回调函数。
下面是一个例子,服务器使用 Promise 处理请求,然后使用finally方法关掉服务器。
server.listen(port).then(function () {
`// ...`
}).finally(server.stop);
finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。
Promise.all()方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.all([p1, p2, p3]);
上面代码中,Promise.all()方法接受一个数组作为参数,p1、p2、p3都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。另外,Promise.all()方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。
p的状态由p1、p2、p3决定,分成两种情况。
(1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
(2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。
Promise.race()方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。
复制代码
const p = Promise.race([p1, p2, p3]);
上面代码中,只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。
Promise.race()方法的参数与Promise.all()方法一样,如果不是 Promise 实例,就会先调用下面讲到的Promise.resolve()方法,将参数转为 Promise 实例,再进一步处理。
下面是一个例子,如果指定时间内没有获得结果,就将 Promise 的状态变为reject,否则变为resolve。
-
const p = Promise.race([ -
fetch('/resource-that-may-take-a-while'), -
new Promise(function (resolve, reject) {
`setTimeout(() => reject(new Error('request timeout')), 5000)`
-
}) -
]); -
p -
.then(console.log) -
.catch(console.error);
上面代码中,如果 5 秒之内fetch方法无法返回结果,变量p的状态就会变为rejected,从而触发catch方法指定的回调函数。
Promise.allSettled()
Promise.allSettled()方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只有等到所有这些参数实例都返回结果,不管是fulfilled还是rejected,包装实例才会结束。该方法由 ES2020 引入。
-
const promises = [ -
fetch('/api-1'), -
fetch('/api-2'), -
fetch('/api-3'), -
]; -
await Promise.allSettled(promises); -
removeLoadingIndicator();
上面代码对服务器发出三个请求,等到三个请求都结束,不管请求成功还是失败,加载的滚动图标就会消失。
该方法返回的新的 Promise 实例,一旦结束,状态总是fulfilled,不会变成rejected。状态变成fulfilled后,Promise 的监听函数接收到的参数是一个数组,每个成员对应一个传入Promise.allSettled()的 Promise 实例。
-
const resolved = Promise.resolve(42); -
const rejected = Promise.reject(-1); -
const allSettledPromise = Promise.allSettled([resolved, rejected]); -
allSettledPromise.then(function (results) { -
console.log(results); -
}); -
// [ -
// { status: 'fulfilled', value: 42 }, -
// { status: 'rejected', reason: -1 } -
// ]
上面代码中,Promise.allSettled()的返回值allSettledPromise,状态只可能变成fulfilled。它的监听函数接收到的参数是数组results。该数组的每个成员都是一个对象,对应传入Promise.allSettled()的两个 Promise 实例。每个对象都有status属性,该属性的值只可能是字符串fulfilled或字符串rejected。fulfilled时,对象有value属性,rejected时有reason属性,对应两种状态的返回值。
下面是返回值用法的例子。
-
const promises = [ fetch('index.html'), fetch('https://does-not-exist/') ]; -
const results = await Promise.allSettled(promises); -
// 过滤出成功的请求 -
const successfulPromises = results.filter(p => p.status === 'fulfilled'); -
// 过滤出失败的请求,并输出原因 -
const errors = results -
.filter(p => p.status === 'rejected') -
.map(p => p.reason);
有时候,我们不关心异步操作的结果,只关心这些操作有没有结束。这时,Promise.allSettled()方法就很有用。如果没有这个方法,想要确保所有操作都结束,就很麻烦。Promise.all()方法无法做到这一点。
-
const urls = [ /* ... */ ]; -
const requests = urls.map(x => fetch(x)); -
try { -
await Promise.all(requests); -
console.log('所有请求都成功。'); -
} catch { -
console.log('至少一个请求失败,其他请求可能还没结束。'); -
}
上面代码中,Promise.all()无法确定所有请求都结束。想要达到这个目的,写起来很麻烦,有了Promise.allSettled(),这就很容易了。
Promise.any()方法接受一组 Promise 实例作为参数,包装成一个新的 Promise 实例。只要参数实例有一个变成fulfilled状态,包装实例就会变成fulfilled状态;如果所有参数实例都变成rejected状态,包装实例就会变成rejected状态。该方法目前是一个第三阶段的提案 。
Promise.any()跟Promise.race()方法很像,只有一点不同,就是不会因为某个 Promise 变成rejected状态而结束。
-
var resolved = Promise.resolve(42); -
var rejected = Promise.reject(-1); -
var alsoRejected = Promise.reject(Infinity); -
Promise.any([resolved, rejected, alsoRejected]).then(function (result) { -
console.log(result); // 42 -
}); -
Promise.any([rejected, alsoRejected]).catch(function (results) { -
console.log(results); // [-1, Infinity] -
});
17. Iterator 和 for ... of 循环
扩展运算符
只要某个数据结构部署了 Iterator 接口,就可以对它使用扩展运算符,将其转为数组。
let arr = [...iterable];
yield
yield*后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。
-
let generator = function* () { -
yield 1; -
yield* [2,3,4]; -
yield 5; -
}; -
var iterator = generator(); -
iterator.next() // { value: 1, done: false } -
iterator.next() // { value: 2, done: false } -
iterator.next() // { value: 3, done: false } -
iterator.next() // { value: 4, done: false } -
iterator.next() // { value: 5, done: false } -
iterator.next() // { value: undefined, done: true }
其他场合
由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口。下面是一些例子。
- for...of
- Array.from()
- Map(), Set(), WeakMap(), WeakSet()(比如
new Map([['a',1],['b',2]])) - Promise.all()
- Promise.race()
generator 函数
-
let myIterable = { -
[Symbol.iterator]: function* () { -
yield 1; -
yield 2; -
yield 3; -
} -
} -
[...myIterable] // [1, 2, 3] -
// 或者采用下面的简洁写法 -
let obj = { -
* [Symbol.iterator]() { -
yield 'hello'; -
yield 'world'; -
} -
}; -
for (let x of obj) { -
console.log(x); -
} -
// "hello" -
// "world"
遍历器对象除了具有next方法,还可以具有return方法和throw方法。如果你自己写遍历器对象生成函数,那么next方法是必须部署的,return方法和throw方法是否部署是可选的。
return方法的使用场合是,如果for...of循环提前退出(通常是因为出错,或者有break语句),就会调用return方法。如果一个对象在完成遍历前,需要清理或释放资源,就可以部署return方法。
-
function readLinesSync(file) { -
return {
`[Symbol.iterator]() {`
`return {`
`next() {`
`return { done: false };`
`},`
`return() {`
`file.close();`
`return { done: true };`
`}`
`};`
`},`
-
}; -
}上面代码中,函数
readLinesSync接受一个文件对象作为参数,返回一个遍历器对象,其中除了next方法,还部署了return方法。下面的两种情况,都会触发执行return方法。 -
// 情况一 -
for (let line of readLinesSync(fileName)) { -
console.log(line); -
break; -
} -
// 情况二 -
for (let line of readLinesSync(fileName)) { -
console.log(line); -
throw new Error(); -
}
18. Generator 函数
函数的写法如下:
function* foo(x, y) { ··· }
yield 表达式
由于 Generator 函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志。
遍历器对象的next方法的运行逻辑如下。
(1)遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
(2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。
(3)如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
(4)如果该函数没有return语句,则返回的对象的value属性值为undefined。
需要注意的是,yield表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的"惰性求值"(Lazy Evaluation)的语法功能。
复制代码
function* gen() {yield 123 + 456;}
上面代码中,yield后面的表达式123 + 456,不会立即求值,只会在next方法将指针移到这一句时,才会求值。
yield表达式与return语句既有相似之处,也有区别。相似之处在于,都能返回紧跟在语句后面的那个表达式的值。区别在于每次遇到yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能。一个函数里面,只能执行一次(或者说一个)return语句,但是可以执行多次(或者说多个)yield表达式。正常函数只能返回一个值,因为只能执行一次return;Generator 函数可以返回一系列的值,因为可以有任意多个yield。从另一个角度看,也可以说 Generator 生成了一系列的值,这也就是它的名称的来历(英语中,generator 这个词是"生成器"的意思)。
Generator 函数可以不用yield表达式,这时就变成了一个单纯的暂缓执行函数。
-
function* f() { -
console.log('执行了!') -
} -
var generator = f(); -
setTimeout(function () { -
generator.next() -
}, 2000);
上面代码中,函数f如果是普通函数,在为变量generator赋值时就会执行。但是,函数f是一个 Generator 函数,就变成只有调用next方法时,函数f才会执行。
另外需要注意,yield表达式只能用在 Generator 函数里面,用在其他地方都会报错。
Generator 是实现状态机的最佳结构。比如,下面的clock函数就是一个状态机。
var ticking = true;var clock = function() {if (ticking)
`console.log('Tick!');`
else
`console.log('Tock!');`
ticking = !ticking;}
上面代码的clock函数一共有两种状态(Tick和Tock),每运行一次,就改变一次状态。这个函数如果用 Generator 实现,就是下面这样。
var clock = function* () {while (true) {
`console.log('Tick!');`
`yield;`
`console.log('Tock!');`
`yield;`
}};
18. Generator 函数的语法 - 应用 - 《阮一峰 ECMAScript 6 (ES6) 标准入门教程 第三版》 - 书栈网 · BookStack
generator 控制流
-
scheduler(longRunningTask(initialValue)); -
function scheduler(task) { -
var taskObj = task.next(task.value); -
// 如果Generator函数未结束,就继续调用 -
if (!taskObj.done) { -
task.value = taskObj.value -
scheduler(task); -
} -
} -
let steps = [step1Func, step2Func, step3Func]; -
function* iterateSteps(steps){ -
for (var i=0; i< steps.length; i++){ -
var step = steps[i]; -
yield step(); -
} -
}
for ... of 无法遍历return 对象
for...of循环可以自动遍历 Generator 函数运行时生成的Iterator对象,且此时不再需要调用next方法。
-
function* foo() { -
yield 1; -
yield 2; -
yield 3; -
yield 4; -
yield 5; -
return 6; -
} -
for (let v of foo()) { -
console.log(v); -
} -
// 1 2 3 4 5
上面代码使用for...of循环,依次显示 5 个yield表达式的值。这里需要注意,一旦next方法的返回对象的done属性为true,for...of循环就会中止,且不包含该返回对象,所以上面代码的return语句返回的6,不包括在for...of循环之中。
Generator 函数返回的遍历器对象,还有一个return方法,可以返回给定的值,并且终结遍历 Generator 函数。
function* numbers () {yield 1;try {yield 2;yield 3;} finally {yield 4;yield 5;}yield 6;}var g = numbers();g.next() // { value: 1, done: false }g.next() // { value: 2, done: false }g.return(7) // { value: 4, done: false }g.next() // { value: 5, done: false }g.next() // { value: 7, done: true }
上面代码中,调用return()方法后,就开始执行finally代码块,不执行try里面剩下的代码了,然后等到finally代码块执行完,再返回return()方法指定的返回值。
next()、throw()、return()这三个方法本质上是同一件事,可以放在一起理解。它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换yield表达式。
next()是将yield表达式替换成一个值。
-
const g = function* (x, y) { -
let result = yield x + y; -
return result; -
}; -
const gen = g(1, 2); -
gen.next(); // Object {value: 3, done: false} -
gen.next(1); // Object {value: 1, done: true} -
// 相当于将 let result = yield x + y -
// 替换成 let result = 1;
上面代码中,第二个next(1)方法就相当于将yield表达式替换成一个值1。如果next方法没有参数,就相当于替换成undefined。
throw()是将yield表达式替换成一个throw语句。
gen.throw(new Error('出错了')); // Uncaught Error: 出错了// 相当于将 let result = yield x + y// 替换成 let result = throw(new Error('出错了'));
return()是将yield表达式替换成一个return语句。
gen.return(2); // Object {value: 2, done: true}// 相当于将 let result = yield x + y// 替换成 let result = return 2;
yield*表达式,用来在一个 Generator 函数里面执行另一个 Generator 函数。
yield*命令可以很方便地取出嵌套数组的所有成员。
复制代码
-
function* iterTree(tree) { -
if (Array.isArray(tree)) {
`for(let i=0; i < tree.length; i++) {`
`yield* iterTree(tree[i]);`
`}`
-
} else {
`yield tree;`
-
} -
} -
const tree = [ 'a', ['b', 'c'], ['d', 'e'] ]; -
for(let x of iterTree(tree)) { -
console.log(x); -
} -
// a -
// b -
// c -
// d -
// e
由于扩展运算符...默认调用 Iterator 接口,所以上面这个函数也可以用于嵌套数组的平铺。
[...iterTree(tree)] // ["a", "b", "c", "d", "e"]
this 结合 generator :
-
function* F() { -
this.a = 1; -
yield this.b = 2; -
yield this.c = 3; -
} -
var f = F.call(F.prototype); -
f.next(); // Object {value: 2, done: false} -
f.next(); // Object {value: 3, done: false} -
f.next(); // Object {value: undefined, done: true} -
f.a // 1 -
f.b // 2 -
f.c // 3
19. Generator 函数的异步应用
Generator 函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因。除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案:函数体内外的数据交换和错误处理机制。
下面看看如何使用 Generator 函数,执行一个真实的异步任务。
-
var fetch = require('node-fetch'); -
function* gen(){ -
var url = 'https://api.github.com/users/github'; -
var result = yield fetch(url); -
console.log(result.bio); -
}
上面代码中,Generator 函数封装了一个异步操作,该操作先读取一个远程接口,然后从 JSON 格式的数据解析信息。就像前面说过的,这段代码非常像同步操作,除了加上了yield命令。
执行这段代码的方法如下。
-
var g = gen(); -
var result = g.next(); -
result.value.then(function(data){ -
return data.json(); -
}).then(function(data){ -
g.next(data); -
});
上面代码中,首先执行 Generator 函数,获取遍历器对象,然后使用next方法(第二行),执行异步任务的第一阶段。由于Fetch模块返回的是一个 Promise 对象,因此要用then方法调用下一个next方法。
Thunk 函数是自动执行 Generator 函数的一种方法。
传值调用和传名调用
"传值调用"(call by value),即在进入函数体之前,就计算x + 5的值(等于 6),再将这个值传入函数f。C 语言就采用这种策略。
"传名调用"(call by name),即直接将表达式x + 5传入函数体,只在用到它的时候求值。Haskell 语言采用这种策略。
Thunk 函数真正的威力,在于可以自动执行 Generator 函数。下面就是一个基于 Thunk 函数的 Generator 执行器。
-
function run(fn) { -
var gen = fn(); -
function next(err, data) {
`var result = gen.next(data);`
`if (result.done) return;`
`result.value(next);`
-
} -
next(); -
} -
function* g() { -
// ... -
} -
run(g);
上面代码的run函数,就是一个 Generator 函数的自动执行器。内部的next函数就是 Thunk 的回调函数。next函数先将指针移到 Generator 函数的下一步(gen.next方法),然后判断 Generator 函数是否结束(result.done属性),如果没结束,就将next函数再传入 Thunk 函数(result.value属性),否则就直接退出。
co 模块是著名程序员 TJ Holowaychuk 于 2013 年 6 月发布的一个小工具,用于 Generator 函数的自动执行。
下面是一个 Generator 函数,用于依次读取两个文件。
var gen = function* () {var f1 = yield readFile('/etc/fstab');var f2 = yield readFile('/etc/shells');console.log(f1.toString());console.log(f2.toString());};
co 模块可以让你不用编写 Generator 函数的执行器。
var co = require('co');co(gen);
co就是把对象转化为promise对象如何层层then
20. async 函数
async 函数是什么?一句话,它就是 Generator 函数的语法糖。
前文有一个 Generator 函数,依次读取两个文件。
-
const fs = require('fs'); -
const readFile = function (fileName) { -
return new Promise(function (resolve, reject) {
`fs.readFile(fileName, function(error, data) {`
`if (error) return reject(error);`
`resolve(data);`
`});`
-
}); -
}; -
const gen = function* () { -
const f1 = yield readFile('/etc/fstab'); -
const f2 = yield readFile('/etc/shells'); -
console.log(f1.toString()); -
console.log(f2.toString()); -
};
上面代码的函数gen可以写成async函数,就是下面这样。
const asyncReadFile = async function () {const f1 = await readFile('/etc/fstab');const f2 = await readFile('/etc/shells');console.log(f1.toString());console.log(f2.toString());};
一比较就会发现,async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await,仅此而已。
ad
async函数对 Generator 函数的改进,体现在以下四点。
(1)内置执行器。
Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。
asyncReadFile();
上面的代码调用了asyncReadFile函数,然后它就会自动执行,输出最后结果。这完全不像 Generator 函数,需要调用next方法,或者用co模块,才能真正执行,得到最后结果。
(2)更好的语义。
async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。
(3)更广的适用性。
co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。
(4)返回值是 Promise。
async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。
进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。
async 函数有多种使用形式。
-
// 函数声明 -
async function foo() {} -
// 函数表达式 -
const foo = async function () {}; -
// 对象的方法 -
let obj = { async foo() {} }; -
obj.foo().then(...) -
// Class 的方法 -
class Storage { -
constructor() { -
this.cachePromise = caches.open('avatars'); -
} -
async getAvatar(name) { -
const cache = await this.cachePromise; -
return cache.match(`/avatars/${name}.jpg`); -
} -
} -
const storage = new Storage(); -
storage.getAvatar('jake').then(...); -
// 箭头函数 -
const foo = async () => {};
Promise 对象的状态变化
async函数返回的 Promise 对象,必须等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。
下面是一个例子。
async function getTitle(url) {let response = await fetch(url);let html = await response.text();return html.match(/<title>([\s\S]+)<\/title>/i)[1];}getTitle('https://tc39.github.io/ecma262/').then(console.log)// "ECMAScript 2017 Language Specification"
上面代码中,函数getTitle内部有三个操作:抓取网页、取出文本、匹配页面标题。只有这三个操作全部完成,才会执行then方法里面的console.log。
await 命令
正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。另一种情况是,await命令后面是一个thenable对象(即定义了then方法的对象),那么await会将其等同于 Promise 对象。
任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行。
另一种方法是await后面的 Promise 对象再跟一个catch方法,处理前面可能出现的错误。
async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。
-
async function fn(args) { -
// ... -
} -
// 等同于 -
function fn(args) { -
return spawn(function* () {
`// ...`
-
}); -
}
所有的async函数都可以写成上面的第二种形式,其中的spawn函数就是自动执行器。
下面给出spawn函数的实现,基本就是前文自动执行器的翻版。
复制代码
function spawn(genF) {return new Promise(function(resolve, reject) {
`const gen = genF();`
`function step(nextF) {`
`let next;`
`try {`
`next = nextF();`
`} catch(e) {`
`return reject(e);`
`}`
`if(next.done) {`
`return resolve(next.value);`
`}`
`Promise.resolve(next.value).then(function(v) {`
`step(function() { return gen.next(v); });`
`}, function(e) {`
`step(function() { return gen.throw(e); });`
`});`
`}`
`step(function() { return gen.next(undefined); });`
});}
三种异步的比较
我们通过一个例子,来看 async 函数与 Promise、Generator 函数的比较。
假定某个 DOM 元素上面,部署了一系列的动画,前一个动画结束,才能开始后一个。如果当中有一个动画出错,就不再往下执行,返回上一个成功执行的动画的返回值。
首先是 Promise 的写法。
-
function chainAnimationsPromise(elem, animations) { -
// 变量ret用来保存上一个动画的返回值 -
let ret = null; -
// 新建一个空的Promise -
let p = Promise.resolve(); -
// 使用then方法,添加所有动画 -
for(let anim of animations) {
`p = p.then(function(val) {`
`ret = val;`
`return anim(elem);`
`});`
-
} -
// 返回一个部署了错误捕捉机制的Promise -
return p.catch(function(e) {
`/* 忽略错误,继续执行 */`
-
}).then(function() {
`return ret;`
-
}); -
}
虽然 Promise 的写法比回调函数的写法大大改进,但是一眼看上去,代码完全都是 Promise 的 API(then、catch等等),操作本身的语义反而不容易看出来。
接着是 Generator 函数的写法。
-
function chainAnimationsGenerator(elem, animations) { -
return spawn(function*() {
`let ret = null;`
`try {`
`for(let anim of animations) {`
`ret = yield anim(elem);`
`}`
`} catch(e) {`
`/* 忽略错误,继续执行 */`
`}`
`return ret;`
-
}); -
}
上面代码使用 Generator 函数遍历了每个动画,语义比 Promise 写法更清晰,用户定义的操作全部都出现在spawn函数的内部。这个写法的问题在于,必须有一个任务运行器,自动执行 Generator 函数,上面代码的spawn函数就是自动执行器,它返回一个 Promise 对象,而且必须保证yield语句后面的表达式,必须返回一个 Promise。
最后是 async 函数的写法。
async function chainAnimationsAsync(elem, animations) {let ret = null;try {
`for(let anim of animations) {`
`ret = await anim(elem);`
`}`
} catch(e) {
`/* 忽略错误,继续执行 */`
}return ret;}
可以看到 Async 函数的实现最简洁,最符合语义,几乎没有语义不相关的代码。它将 Generator 写法中的自动执行器,改在语言层面提供,不暴露给用户,因此代码量最少。如果使用 Generator 写法,自动执行器需要用户自己提供。
顺序完成异步操作
-
async function logInOrder(urls) { -
// 并发读取远程URL -
const textPromises = urls.map(async url => { -
const response = await fetch(url); -
return response.text(); -
}); -
// 按次序输出 -
for (const textPromise of textPromises) { -
console.log(await textPromise); -
} -
}
顶层await
// awaiting.jslet output;export default (async function main() {const dynamic = await import(someMission);const data = await fetch(url);output = someProcess(dynamic.default, data);})();export { output };
21. Class 的基本语法
原始:
-
function Point(x, y) { -
this.x = x; -
this.y = y; -
} -
Point.prototype.toString = function () { -
return '(' + this.x + ', ' + this.y + ')'; -
}; -
var p = new Point(1, 2);
es6改进后:
-
class Point { -
constructor(x, y) { -
this.x = x; -
this.y = y; -
} -
toString() { -
return '(' + this.x + ', ' + this.y + ')'; -
} -
}
上面代码定义了一个"类",定义"类"的方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。另外,方法之间不需要逗号分隔,加了会报错。
类的数据类型就是函数,类本身就指向构造函数。
-
class Bar { -
doStuff() { -
console.log('stuff'); -
} -
} -
var b = new Bar(); -
b.doStuff() // "stuff"
构造函数的prototype属性,在 ES6 的"类"上面继续存在。事实上,类的所有方法都定义在类的prototype属性上面。
-
class Point { -
constructor() { -
// ... -
} -
toString() { -
// ... -
} -
toValue() { -
// ... -
} -
} -
// 等同于 -
Point.prototype = { -
constructor() {}, -
toString() {}, -
toValue() {}, -
};
prototype对象的constructor属性,直接指向"类"的本身
Point.prototype.constructor === Point // true
类是有函数构造的q
-
//定义类 -
class Point { -
constructor(x, y) { -
this.x = x; -
this.y = y; -
} -
toString() { -
return '(' + this.x + ', ' + this.y + ')'; -
} -
} -
var point = new Point(2, 3); -
point.toString() // (2, 3) -
point.hasOwnProperty('x') // true -
point.hasOwnProperty('y') // true -
point.hasOwnProperty('toString') // false -
point.__proto__.hasOwnProperty('toString') // true
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为"静态方法"。
-
class Foo { -
static classMethod() {
`return 'hello';`
-
} -
} -
Foo.classMethod() // 'hello' -
var foo = new Foo(); -
foo.classMethod() -
// TypeError: foo.classMethod is not a function
Foo类的classMethod方法前有static关键字,表明该方法是一个静态方法,可以直接在Foo类上调用(Foo.classMethod()),而不是在Foo类的实例上调用。如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法。
-
class Foo { -
static bar() {
`this.baz();`
-
} -
static baz() {
`console.log('hello');`
-
} -
baz() {
`console.log('world');`
-
} -
} -
Foo.bar() // hello上面代码中,静态方法
bar调用了this.baz,这里的this指的是Foo类,而不是Foo的实例,等同于调用Foo.baz。另外,从这个例子还可以看出,静态方法可以与非静态方法重名。 -
class Foo { -
static classMethod() {
`return 'hello';`
-
} -
} -
class Bar extends Foo { -
} -
Bar.classMethod() // 'hello'
实例属性的新写法:可以不使用constructor, 而是直接写在顶层
-
class IncreasingCounter { -
constructor() {
`this._count = 0;`
-
} -
get value() {
`console.log('Getting the current value!');`
`return this._count;`
-
} -
increment() {
`this._count++;`
-
} -
} -
class IncreasingCounter { -
_count = 0; -
get value() {
`console.log('Getting the current value!');`
`return this._count;`
-
} -
increment() {
`this._count++;`
-
} -
}
静态属性
-
class Foo { -
} -
Foo.prop = 1; -
Foo.prop // 1 -
class MyClass { -
static myStaticProp = 42; -
constructor() {
`console.log(MyClass.myStaticProp); // 42`
-
} -
}
可以在constructor定义属性的前面添加static 设置静态属性
私有属性和私有方法,外部不能访问
class Foo {#a;#b;constructor(a, b) {
`this.#a = a;`
`this.#b = b;`
}#sum() {
`return #a + #b;`
}printSum() {
`console.log(this.#sum());`
}}
new是从构造函数生成实例对象的命令。ES6 为new命令引入了一个new.target属性,该属性一般用在构造函数之中,返回new命令作用于的那个构造函数。如果构造函数不是通过new命令或Reflect.construct()调用的,new.target会返回undefined,因此这个属性可以用来确定构造函数是怎么调用的。
-
function Person(name) { -
if (new.target !== undefined) {
`this.name = name;`
-
} else {
`throw new Error('必须使用 new 命令生成实例');`
-
} -
} -
// 另一种写法 -
function Person(name) { -
if (new.target === Person) {
`this.name = name;`
-
} else {
`throw new Error('必须使用 new 命令生成实例');`
-
} -
} -
var person = new Person('张三'); // 正确 -
var notAPerson = Person.call(person, '张三'); // 报错
new.target 是用来检测是否是由new构成的,区别于call构成
new.target会返回子类。
用法:
-
class Shape { -
constructor() {
`if (new.target === Shape) {`
`throw new Error('本类不能实例化');`
`}`
-
} -
} -
class Rectangle extends Shape { -
constructor(length, width) {
`super();`
`// ...`
-
} -
} -
var x = new Shape(); // 报错 -
var y = new Rectangle(3, 4); // 正确
22. Class 的继承
-
class Point { -
} -
class ColorPoint extends Point { -
}
子类在constructor中必须使用super() 可以调用父类的constructor
-
class ColorPoint extends Point { -
constructor(x, y, color) {
`super(x, y); // 调用父类的constructor(x, y)`
`this.color = color;`
-
} -
toString() {
`return this.color + ' ' + super.toString(); // 调用父类的toString()`
-
} -
}
如果子类没有定义constructor方法,这个方法会被默认添加;
-
class ColorPoint extends Point { -
} -
// 等同于 -
class ColorPoint extends Point { -
constructor(...args) {
`super(...args);`
-
} -
}
同时需要注意的是只有在使用super后才可以使用this关键字
-
let cp = new ColorPoint(25, 8, 'green'); -
cp instanceof ColorPoint // true -
cp instanceof Point // true
实例对象cp同时是子类和父类ColorPoint和Point两个类的实例
Object.getPrototypeOf方法可以用来从子类上获取父类。
super() 只能放在 constructor中
-
class A { -
p() {
`return 2;`
-
} -
} -
class B extends A { -
constructor() {
`super();`
`console.log(super.p()); // 2`
-
} -
} -
let b = new B();
上面代码中,子类B当中的super.p(),就是将super当作一个对象使用。这时,super在普通方法之中,指向A.prototype,所以super.p()就相当于A.prototype.p()。
-
class A { -
constructor() {
`this.x = 1;`
-
} -
print() {
`console.log(this.x);`
-
} -
} -
class B extends A { -
constructor() {
`super();`
`this.x = 2;`
-
} -
m() {
`super.print();`
-
} -
} -
let b = new B(); -
b.m() // 2
上面代码中,super.print()虽然调用的是A.prototype.print(),但是A.prototype.print()内部的this指向子类B的实例,导致输出的是2,而不是1。也就是说,实际上执行的是super.print.call(this)。
由于this指向子类实例,所以如果通过super对某个属性赋值,这时super就是this,赋值的属性会变成子类实例的属性。
-
class A { -
constructor() {
`this.x = 1;`
-
} -
} -
class B extends A { -
constructor() {
`super();`
`this.x = 2;`
`super.x = 3;`
`console.log(super.x); // undefined`
`console.log(this.x); // 3`
-
} -
} -
let b = new B();
上面代码中,super.x赋值为3,这时等同于对this.x赋值为3。而当读取super.x的时候,读的是A.prototype.x,所以返回undefined。
如果super作为对象,用在静态方法之中,这时super将指向父类,而不是父类的原型对象。
-
class Parent { -
static myMethod(msg) {
`console.log('static', msg);`
-
} -
myMethod(msg) {
`console.log('instance', msg);`
-
} -
} -
class Child extends Parent { -
static myMethod(msg) {
`super.myMethod(msg);`
-
} -
myMethod(msg) {
`super.myMethod(msg);`
-
} -
} -
Child.myMethod(1); // static 1 -
var child = new Child(); -
child.myMethod(2); // instance 2
(1)子类的__proto__属性,表示构造函数的继承,总是指向父类。
(2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。
-
class A { -
} -
class B extends A { -
} -
B.__proto__ === A // true -
B.prototype.__proto__ === A.prototype // true
继承原生构造函数:
- Boolean()
- Number()
- String()
- Array()
- Date()
- Function()
- RegExp()
- Error()
- Object()
-
class MyArray extends Array { -
constructor(...args) {
`super(...args);`
-
} -
} -
var arr = new MyArray(); -
arr[0] = 12; -
arr.length // 1 -
arr.length = 0; -
arr[0] // undefined
Mixin 指的是多个对象合成一个新的对象,新对象具有各个组成成员的接口。它的最简单实现如下。
const a = {a: 'a'};const b = {b: 'b'};const c = {...a, ...b}; // {a: 'a', b: 'b'}
23. Module的语法
ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。
// ES6模块import { stat, exists, readFile } from 'fs';
下面是import()的一些适用场合。
(1)按需加载。
import()可以在需要的时候,再加载某个模块。
button.addEventListener('click', event => {import('./dialogBox.js').then(dialogBox => {
`dialogBox.open();`
}).catch(error => {
`/* Error handling */`
})});
上面代码中,import()方法放在click事件的监听函数之中,只有用户点击了按钮,才会加载这个模块。
(2)条件加载
import()可以放在if代码块,根据不同的情况,加载不同的模块。
if (condition) {import('moduleA').then(...);} else {import('moduleB').then(...);}
上面代码中,如果满足条件,就加载模块 A,否则加载模块 B。
(3)动态的模块路径
import()允许模块路径动态生成。
import(f()).then(...);
上面代码中,根据函数f的返回结果,加载不同的模块。
注意点
import()加载模块成功以后,这个模块会作为一个对象,当作then方法的参数。因此,可以使用对象解构赋值的语法,获取输出接口。
import('./myModule.js').then(({export1, export2}) => {// ...·});
上面代码中,export1和export2都是myModule.js的输出接口,可以解构获得。
如果模块有default输出接口,可以用参数直接获得。
import('./myModule.js').then(myModule => {console.log(myModule.default);});
上面的代码也可以使用具名输入的形式。
import('./myModule.js').then(({default: theDefault}) => {console.log(theDefault);});
如果想同时加载多个模块,可以采用下面的写法。
Promise.all([import('./module1.js'),import('./module2.js'),import('./module3.js'),]).then(([module1, module2, module3]) => {···});
import()也可以用在 async 函数之中。
async function main() {const myModule = await import('./myModule.js');const {export1, export2} = await import('./myModule.js');const [module1, module2, module3] =
`await Promise.all([`
`import('./module1.js'),`
`import('./module2.js'),`
`import('./module3.js'),`
`]);`
}main();
严格模式:
ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";。
严格模式主要有以下限制。
- 变量必须声明后再使用
- 函数的参数不能有同名属性,否则报错
- 不能使用
with语句 - 不能对只读属性赋值,否则报错
- 不能使用前缀 0 表示八进制数,否则报错
- 不能删除不可删除的属性,否则报错
- 不能删除变量
delete prop,会报错,只能删除属性delete global[prop] eval不会在它的外层作用域引入变量eval和arguments不能被重新赋值arguments不会自动反映函数参数的变化- 不能使用
arguments.callee - 不能使用
arguments.caller - 禁止
this指向全局对象 - 不能使用
fn.caller和fn.arguments获取函数调用的堆栈 - 增加了保留字(比如
protected、static和interface)
上面这些限制,模块都必须遵守。由于严格模式是 ES5 引入的,不属于 ES6,所以请参阅相关 ES5 书籍,本书不再详细介绍了。
其中,尤其需要注意this的限制。ES6 模块之中,顶层的this指向undefined,即不应该在顶层代码使用this。
export:
模块功能主要由两个命令构成:export和import。export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。
一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。下面是一个 JS 文件,里面使用export命令输出变量。
// profile.jsexport var firstName = 'Michael';export var lastName = 'Jackson';export var year = 1958;
上面代码是profile.js文件,保存了用户信息。ES6 将其视为一个模块,里面用export命令对外部输出了三个变量。
export的写法,除了像上面这样,还有另外一种。
-
// profile.js -
var firstName = 'Michael'; -
var lastName = 'Jackson'; -
var year = 1958; -
export { firstName, lastName, year }; -
function v1() { ... } -
function v2() { ... } -
export { -
v1 as streamV1, -
v2 as streamV2, -
v2 as streamLatestVersion -
}; -
// 写法一 -
export var m = 1; -
// 写法二 -
var m = 1; -
export {m}; -
// 写法三 -
var n = 1; -
export {n as m}; -
// 报错 -
function f() {} -
export f; -
// 正确 -
export function f() {}; -
// 正确 -
function f() {} -
export {f};
另外,export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。
最后,export命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,下一节的import命令也是如此。这是因为处于条件代码块之中,就没法做静态优化了,违背了 ES6 模块的设计初衷。
-
function foo() { -
export default 'bar' // SyntaxError -
} -
foo()上面代码中,
export语句放在函数之中,结果报错。 -
// main.js -
import { firstName, lastName, year } from './profile.js'; -
function setName(element) { -
element.textContent = firstName + ' ' + lastName; -
}
如果想为输入的变量重新取一个名字,import命令要使用as关键字,将输入的变量重命名。
import { lastName as surname } from './profile.js';
import命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。
-
import {a} from './xxx.js' -
a = {}; // Syntax Error : 'a' is read-only;
上面代码中,脚本加载了变量a,对其重新赋值就会报错,因为a是一个只读的接口。但是,如果a是一个对象,改写a的属性是允许的。
-
import {a} from './xxx.js' -
a.foo = 'hello'; // 合法操作
由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。
-
// 报错 -
import { 'f' + 'oo' } from 'my_module'; -
// 报错 -
let module = 'my_module'; -
import { foo } from module; -
// 报错 -
if (x === 1) { -
import { foo } from 'module1'; -
} else { -
import { foo } from 'module2'; -
} -
import * as circle from './circle'; -
console.log('圆面积:' + circle.area(4)); -
console.log('圆周长:' + circle.circumference(14));
注意,模块整体加载所在的那个对象(上例是circle),应该是可以静态分析的,所以不允许运行时改变。下面的写法都是不允许的。
-
import * as circle from './circle'; -
// 下面两行都是不允许的 -
circle.foo = 'hello'; -
circle.area = function () {};
export default:
-
// modules.js -
function add(x, y) { -
return x * y; -
} -
export {add as default}; -
// 等同于 -
// export default add; -
// app.js -
import { default as foo } from 'modules'; -
// 等同于 -
// import foo from 'modules';
有了export default命令,输入模块时就非常直观了,以输入 lodash 模块为例。
import _ from 'lodash';
如果想在一条import语句中,同时输入默认方法和其他接口,可以写成下面这样。
import _, { each, forEach } from 'lodash';
export 与 import 的复合写法
-
export { foo, bar } from 'my_module'; -
// 可以简单理解为 -
import { foo, bar } from 'my_module'; -
export { foo, bar };
上面代码中,export和import语句可以结合在一起,写成一行。但需要注意的是,写成一行以后,foo和bar实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foo和bar。
-
// 接口改名 -
export { foo as myFoo } from 'my_module'; -
// 整体输出 -
export * from 'my_module'; -
// 默认接口 -
export { default } from 'foo';
具名接口改为默认接口的写法如下。
-
export { es6 as default } from './someModule'; -
// 等同于 -
import { es6 } from './someModule'; -
export default es6;
模块的继承:
-
// circleplus.js -
export * from 'circle'; -
export var e = 2.71828182846; -
export default function(x) { -
return Math.exp(x); -
}
上面代码中的export *,表示再输出circle模块的所有属性和方法。注意,export *命令会忽略circle模块的default方法。然后,上面代码又输出了自定义的e变量和默认方法。
如果要使用的常量非常多,可以建一个专门的constants目录,将各种常量写在不同的文件里面,保存在该目录下。
-
// constants/db.js -
export const db = { -
url: 'http://my.couchdbserver.local:5984', -
admin_username: 'admin', -
admin_password: 'admin password' -
}; -
// constants/user.js -
export const users = ['root', 'admin', 'staff', 'ceo', 'chief', 'moderator'];
24. Module 的加载实现
HTML 网页中,浏览器通过<script>标签加载 JavaScript 脚本。
-
<!-- 页面内嵌的脚本 --> -
<script type="application/javascript"> -
// module code -
</script> -
<!-- 外部脚本 --> -
<script type="application/javascript" src="path/to/myModule.js"> -
</script>
上面代码中,由于浏览器脚本的默认语言是 JavaScript,因此type="application/javascript"可以省略。
默认情况下,浏览器是同步加载 JavaScript 脚本,即渲染引擎遇到<script>标签就会停下来,等到执行完脚本,再继续向下渲染。如果是外部脚本,还必须加入脚本下载的时间。
如果脚本体积很大,下载和执行的时间就会很长,因此造成浏览器堵塞,用户会感觉到浏览器"卡死"了,没有任何响应。这显然是很不好的体验,所以浏览器允许脚本异步加载,下面就是两种异步加载的语法。
<script src="path/to/myModule.js" defer></script><script src="path/to/myModule.js" async></script>
defer与async的区别是:defer要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行;async一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。一句话,defer是"渲染完再执行",async是"下载完就执行"。另外,如果有多个defer脚本,会按照它们在页面出现的顺序加载,而多个async脚本是不能保证加载顺序的。
加载规则
浏览器加载 ES6 模块,也使用<script>标签,但是要加入type="module"属性。
<script type="module" src="./foo.js"></script>
上面代码在网页中插入一个模块foo.js,由于type属性设为module,所以浏览器知道这是一个 ES6 模块。
浏览器对于带有type="module"的<script>,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>标签的defer属性。
<script type="module">import $ from "./jquery/src/jquery.js";$('#message').text('Hi from jQuery!');</script>
对于外部的模块脚本(上例是foo.js),有几点需要注意。
- 代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见。
- 模块脚本自动采用严格模式,不管有没有声明
use strict。 - 模块之中,可以使用
import命令加载其他模块(.js后缀不可省略,需要提供绝对 URL 或相对 URL),也可以使用export命令输出对外接口。 - 模块之中,顶层的
this关键字返回undefined,而不是指向window。也就是说,在模块顶层使用this关键字,是无意义的。 - 同一个模块如果加载多次,将只执行一次。
下面是一个示例模块。
-
import utils from 'https://example.com/js/utils.js'; -
const x = 1; -
console.log(x === window.x); //false -
console.log(this === undefined); // true
利用顶层的this等于undefined这个语法点,可以侦测当前代码是否在 ES6 模块之中。
const isNotModuleScript = this !== undefined;
讨论 Node.js 加载 ES6 模块之前,必须了解 ES6 模块与 CommonJS 模块完全不同。
它们有两个重大差异。
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。请看下面这个模块文件lib.js的例子。
// lib.jsvar counter = 3;function incCounter() {counter++;}module.exports = {counter: counter,incCounter: incCounter,};
上面代码输出内部变量counter和改写这个变量的内部方法incCounter。然后,在main.js里面加载这个模块。
-
// main.js -
var mod = require('./lib'); -
console.log(mod.counter); // 3 -
mod.incCounter(); -
console.log(mod.counter); // 3
上面代码说明,lib.js模块加载以后,它的内部变化就影响不到输出的mod.counter了。这是因为mod.counter是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值。
Node.js 加载
Node.js 对 ES6 模块的处理比较麻烦,因为它有自己的 CommonJS 模块格式,与 ES6 模块格式是不兼容的。目前的解决方案是,将两者分开,ES6 模块和 CommonJS 采用各自的加载方案。从 v13.2 版本开始,Node.js 已经默认打开了 ES6 模块支持。
Node.js 要求 ES6 模块采用.mjs后缀文件名。也就是说,只要脚本文件里面使用import或者export命令,那么就必须采用.mjs后缀名。Node.js 遇到.mjs文件,就认为它是 ES6 模块,默认启用严格模式,不必在每个模块文件顶部指定"use strict"。
如果不希望将后缀名改成.mjs,可以在项目的package.json文件中,指定type字段为module。
{"type": "module"}
一旦设置了以后,该目录里面的 JS 脚本,就被解释用 ES6 模块。
# 解释成 ES6 模块$ node my-app.js
如果这时还要使用 CommonJS 模块,那么需要将 CommonJS 脚本的后缀名都改成.cjs。如果没有type字段,或者type字段为commonjs,则.js脚本会被解释成 CommonJS 模块。
总结为一句话:.mjs文件总是以 ES6 模块加载,.cjs文件总是以 CommonJS 模块加载,.js文件的加载取决于package.json里面type字段的设置。
注意,ES6 模块与 CommonJS 模块尽量不要混用。require命令不能加载.mjs文件,会报错,只有import命令才可以加载.mjs文件。反过来,.mjs文件里面也不能使用require命令,必须使用import。
25. 编程风格
尽量不要使用var,而是使用let和const,在let和const之间优选使用const
静态字符串一律使用单引号或反引号,不使用双引号。动态字符串使用反引号。
使用数组成员对变量赋值时,优先使用解构赋值。
单行定义的对象,最后一个成员不以逗号结尾。多行定义的对象,最后一个成员以逗号结尾。
对象尽量静态化,一旦定义,就不得随意添加新的属性。如果添加属性不可避免,要使用Object.assign方法。
使用扩展运算符(...)拷贝数组。
使用 Array.from 方法,将类似数组的对象转为数组。
立即执行函数可以写成箭头函数的形式。
那些使用匿名函数当作参数的场合,尽量用箭头函数代替。因为这样更简洁,而且绑定了 this。
注意区分 Object 和 Map,只有模拟现实世界的实体对象时,才使用 Object。如果只是需要key: value的数据结构,使用 Map 结构。因为 Map 有内建的遍历机制。
-
let map = new Map(arr); -
for (let key of map.keys()) { -
console.log(key); -
} -
for (let value of map.values()) { -
console.log(value); -
} -
for (let item of map.entries()) { -
console.log(item[0], item[1]); -
}
总是用 Class,取代需要 prototype 的操作。因为 Class 的写法更简洁,更易于理解。
使用extends实现继承,因为这样更简单,不会有破坏instanceof运算的危险。
首先,Module 语法是 JavaScript 模块的标准写法,坚持使用这种写法。使用import取代require。
使用export取代module.exports。
如果模块只有一个输出值,就使用export default,如果模块有多个输出值,就不使用export default,export default与普通的export不要同时使用。
不要在模块输入中使用通配符。因为这样可以确保你的模块之中,有一个默认输出(export default)。
-
// bad -
import * as myObject from './importModule'; -
// good -
import myObject from './importModule';
如果模块默认输出一个函数,函数名的首字母应该小写。
如果模块默认输出一个对象,对象名的首字母应该大写。
语法规则和代码风格的检查工具
ESLint 是一个语法规则和代码风格的检查工具,可以用来保证写出语法正确、风格统一的代码。
首先,安装 ESLint。
$ npm i -g eslint
然后,安装 Airbnb 语法规则,以及 import、a11y、react 插件。
$ npm i -g eslint-config-airbnb$ npm i -g eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react
最后,在项目的根目录下新建一个.eslintrc文件,配置 ESLint。
{"extends": "eslint-config-airbnb"}
26. 读懂规格
- Let
ObeToObject(this value).ReturnIfAbrupt(O).- Let
lenbeToLength(Get(O, "length")).ReturnIfAbrupt(len).- If
IsCallable(callbackfn)isfalse, throw a TypeError exception.- If
thisArgwas supplied, letTbethisArg; else letTbeundefined.- Let
AbeArraySpeciesCreate(O, len).ReturnIfAbrupt(A).- Let
kbe 0.- Repeat, while
k<len
- Let
PkbeToString(k).- Let
kPresentbeHasProperty(O, Pk).ReturnIfAbrupt(kPresent).- If
kPresentistrue, then
- Let
kValuebeGet(O, Pk).ReturnIfAbrupt(kValue).- Let
mappedValuebeCall(callbackfn, T, <<kValue, k, O>>).ReturnIfAbrupt(mappedValue).- Let
statusbeCreateDataPropertyOrThrow (A, Pk, mappedValue).ReturnIfAbrupt(status).- Increase
kby 1.- Return
A.
翻译如下。
- 得到当前数组的
this对象- 如果报错就返回
- 求出当前数组的
length属性- 如果报错就返回
- 如果 map 方法的参数
callbackfn不可执行,就报错- 如果 map 方法的参数之中,指定了
this,就让T等于该参数,否则T为undefined- 生成一个新的数组
A,跟当前数组的length属性保持一致- 如果报错就返回
- 设定
k等于 0- 只要
k小于当前数组的length属性,就重复下面步骤
- 设定
Pk等于ToString(k),即将K转为字符串- 设定
kPresent等于HasProperty(O, Pk),即求当前数组有没有指定属性- 如果报错就返回
- 如果
kPresent等于true,则进行下面步骤
- 设定
kValue等于Get(O, Pk),取出当前数组的指定属性- 如果报错就返回
- 设定
mappedValue等于Call(callbackfn, T, <<kValue, k, O>>),即执行回调函数- 如果报错就返回
- 设定
status等于CreateDataPropertyOrThrow (A, Pk, mappedValue),即将回调函数的值放入A数组的指定位置- 如果报错就返回
k增加 1- 返回
A
仔细查看上面的算法,可以发现,当处理一个全是空位的数组时,前面步骤都没有问题。进入第 10 步中第 2 步时,kPresent会报错,因为空位对应的属性名,对于数组来说是不存在的,因此就会返回,不会进行后面的步骤。
27. 异步遍历器
将异步操作包装成 Thunk 函数或者 Promise 对象,即next()方法返回值的value属性是一个 Thunk 函数或者 Promise 对象,等待以后返回真正的值,而done属性则还是同步产生的。
-
function idMaker() { -
let index = 0; -
return {
`next: function() {`
`return {`
`value: new Promise(resolve => setTimeout(() => resolve(index++), 1000)),`
`done: false`
`};`
`}`
-
}; -
} -
const it = idMaker(); -
it.next().value.then(o => console.log(o)) // 1 -
it.next().value.then(o => console.log(o)) // 2 -
it.next().value.then(o => console.log(o)) // 3 -
// ...
上面代码中,value属性的返回值是一个 Promise 对象,用来放置异步操作。但是这样写很麻烦,不太符合直觉,语义也比较绕。
asyncIterator是一个异步遍历器,调用next方法以后,返回一个 Promise 对象。因此,可以使用then方法指定,这个 Promise 对象的状态变为resolve以后的回调函数。回调函数的参数,则是一个具有value和done两个属性的对象,这个跟同步遍历器是一样的。
我们知道,一个对象的同步遍历器的接口,部署在Symbol.iterator属性上面。同样地,对象的异步遍历器接口,部署在Symbol.asyncIterator属性上面。不管是什么样的对象,只要它的Symbol.asyncIterator属性有值,就表示应该对它进行异步遍历。
-
const asyncIterable = createAsyncIterable(['a', 'b']); -
const asyncIterator = asyncIterable[Symbol.asyncIterator](); -
asyncIterator -
.next() -
.then(iterResult1 => { -
console.log(iterResult1); // { value: 'a', done: false } -
return asyncIterator.next(); -
}) -
.then(iterResult2 => { -
console.log(iterResult2); // { value: 'b', done: false } -
return asyncIterator.next(); -
}) -
.then(iterResult3 => { -
console.log(iterResult3); // { value: undefined, done: true } -
}); -
async function f() { -
const asyncIterable = createAsyncIterable(['a', 'b']); -
const asyncIterator = asyncIterable[Symbol.asyncIterator](); -
console.log(await asyncIterator.next()); -
// { value: 'a', done: false } -
console.log(await asyncIterator.next()); -
// { value: 'b', done: false } -
console.log(await asyncIterator.next()); -
// { value: undefined, done: true } -
}
面代码中,next方法用await处理以后,就不必使用then方法了。整个流程已经很接近同步处理了。
注意,异步遍历器的next方法是可以连续调用的,不必等到上一步产生的 Promise 对象resolve以后再调用。这种情况下,next方法会累积起来,自动按照每一步的顺序运行下去。下面是一个例子,把所有的next方法放在Promise.all方法里面。
-
const asyncIterable = createAsyncIterable(['a', 'b']); -
const asyncIterator = asyncIterable[Symbol.asyncIterator](); -
const [{value: v1}, {value: v2}] = await Promise.all([ -
asyncIterator.next(), asyncIterator.next() -
]); -
console.log(v1, v2); // a b
另一种用法是一次性调用所有的next方法,然后await最后一步操作。
-
async function runner() { -
const writer = openFile('someFile.txt'); -
writer.next('hello'); -
writer.next('world'); -
await writer.return(); -
} -
runner();
createAsyncIterable()返回一个拥有异步遍历器接口的对象,for...of循环自动调用这个对象的异步遍历器的next方法,会得到一个 Promise 对象。await用来处理这个 Promise 对象,一旦resolve,就把得到的值(x)传入for...of的循环体。
for await...of循环的一个用途,是部署了 asyncIterable 操作的异步接口,可以直接放入这个循环。
-
let body = ''; -
async function f() { -
for await(const data of req) body += data; -
const parsed = JSON.parse(body); -
console.log('got', parsed); -
}
上面代码中,req是一个 asyncIterable 对象,用来异步读取数据。可以看到,使用for await...of循环以后,代码会非常简洁。
如果next方法返回的 Promise 对象被reject,for await...of就会报错,要用try...catch捕捉。
async function () {try {
`for await (const x of createRejectingIterable()) {`
`console.log(x);`
`}`
} catch (e) {
`console.error(e);`
}}
注意,for await...of循环也可以用于同步遍历器。
(async function () {for await (const x of ['a', 'b']) {
`console.log(x);`
}})();// a// b
异步遍历器的设计目的之一,就是 Generator 函数处理同步操作和异步操作时,能够使用同一套接口。
-
// 同步 Generator 函数 -
function* map(iterable, func) { -
const iter = iterable[Symbol.iterator](); -
while (true) {
`const {value, done} = iter.next();`
`if (done) break;`
`yield func(value);`
-
} -
} -
// 异步 Generator 函数 -
async function* map(iterable, func) { -
const iter = iterable[Symbol.asyncIterator](); -
while (true) {
`const {value, done} = await iter.next();`
`if (done) break;`
`yield func(value);`
-
} -
}
yield*语句也可以跟一个异步遍历器。
-
async function* gen1() { -
yield 'a'; -
yield 'b'; -
return 2; -
} -
async function* gen2() { -
// result 最终会等于 2 -
const result = yield* gen1(); -
}
28. ArrayBuffer
二进制数组由三类对象组成。
(1)ArrayBuffer对象:代表内存之中的一段二进制数据,可以通过"视图"进行操作。"视图"部署了数组接口,这意味着,可以用数组的方法操作内存。
(2)TypedArray视图 :共包括 9 种类型的视图,比如Uint8Array(无符号 8 位整数)数组视图, Int16Array(16 位整数)数组视图, Float32Array(32 位浮点数)数组视图等等。
(3)DataView视图:可以自定义复合格式的视图,比如第一个字节是 Uint8(无符号 8 位整数)、第二、三个字节是 Int16(16 位整数)、第四个字节开始是 Float32(32 位浮点数)等等,此外还可以自定义字节序。
简单说,ArrayBuffer对象代表原始的二进制数据,TypedArray视图用来读写简单类型的二进制数据,DataView视图用来读写复杂类型的二进制数据。
TypedArray视图支持的数据类型一共有 9 种(DataView视图支持除Uint8C以外的其他 8 种)。
| 数据类型 | 字节长度 | 含义 | 对应的 C 语言类型 |
|---|---|---|---|
| Int8 | 1 | 8 位带符号整数 | signed char |
| Uint8 | 1 | 8 位不带符号整数 | unsigned char |
| Uint8C | 1 | 8 位不带符号整数(自动过滤溢出) | unsigned char |
| Int16 | 2 | 16 位带符号整数 | short |
| Uint16 | 2 | 16 位不带符号整数 | unsigned short |
| Int32 | 4 | 32 位带符号整数 | int |
| Uint32 | 4 | 32 位不带符号的整数 | unsigned int |
| Float32 | 4 | 32 位浮点数 | float |
| Float64 | 8 | 64 位浮点数 | double |
ArrayBuffer对象代表储存二进制数据的一段内存,它不能直接读写,只能通过视图(TypedArray视图和DataView视图)来读写,视图的作用是以指定格式解读二进制数据。
ArrayBuffer也是一个构造函数,可以分配一段可以存放数据的连续内存区域。
const buf = new ArrayBuffer(32);
上面代码生成了一段 32 字节的内存区域,每个字节的值默认都是 0。可以看到,ArrayBuffer构造函数的参数是所需要的内存大小(单位字节)。
为了读写这段内容,需要为它指定视图。DataView视图的创建,需要提供ArrayBuffer对象实例作为参数。
const buf = new ArrayBuffer(32);const dataView = new DataView(buf);dataView.getUint8(0) // 0
上面代码对一段 32 字节的内存,建立DataView视图,然后以不带符号的 8 位整数格式,从头读取 8 位二进制数据,结果得到 0,因为原始内存的ArrayBuffer对象,默认所有位都是 0。
另一种TypedArray视图,与DataView视图的一个区别是,它不是一个构造函数,而是一组构造函数,代表不同的数据格式。
-
const buffer = new ArrayBuffer(12); -
const x1 = new Int32Array(buffer); -
x1[0] = 1; -
const x2 = new Uint8Array(buffer); -
x2[0] = 2; -
x1[0] // 2
TypedArray视图的构造函数,除了接受ArrayBuffer实例作为参数,还可以接受普通数组作为参数,直接分配内存生成底层的ArrayBuffer实例,并同时完成对这段内存的赋值。
-
const typedArray = new Uint8Array([0,1,2]); -
typedArray.length // 3 -
typedArray[0] = 5; -
typedArray // [5, 1, 2]
ArrayBuffer实例的byteLength属性,返回所分配的内存区域的字节长度。
const buffer = new ArrayBuffer(32);buffer.byteLength// 32
ArrayBuffer实例有一个slice方法,允许将内存区域的一部分,拷贝生成一个新的ArrayBuffer对象。
const buffer = new ArrayBuffer(8);const newBuffer = buffer.slice(0, 3);
ArrayBuffer有一个静态方法isView,返回一个布尔值,表示参数是否为ArrayBuffer的视图实例。这个方法大致相当于判断参数,是否为TypedArray实例或DataView实例。
-
const buffer = new ArrayBuffer(8); -
ArrayBuffer.isView(buffer) // false -
const v = new Int32Array(buffer); -
ArrayBuffer.isView(v) // true
普通数组的操作方法和属性,对 TypedArray 数组完全适用。
TypedArray.prototype.copyWithin(target, start[, end = this.length])TypedArray.prototype.entries()TypedArray.prototype.every(callbackfn, thisArg?)TypedArray.prototype.fill(value, start=0, end=this.length)TypedArray.prototype.filter(callbackfn, thisArg?)TypedArray.prototype.find(predicate, thisArg?)TypedArray.prototype.findIndex(predicate, thisArg?)TypedArray.prototype.forEach(callbackfn, thisArg?)TypedArray.prototype.indexOf(searchElement, fromIndex=0)TypedArray.prototype.join(separator)TypedArray.prototype.keys()TypedArray.prototype.lastIndexOf(searchElement, fromIndex?)TypedArray.prototype.map(callbackfn, thisArg?)TypedArray.prototype.reduce(callbackfn, initialValue?)TypedArray.prototype.reduceRight(callbackfn, initialValue?)TypedArray.prototype.reverse()TypedArray.prototype.slice(start=0, end=this.length)TypedArray.prototype.some(callbackfn, thisArg?)TypedArray.prototype.sort(comparefn)TypedArray.prototype.toLocaleString(reserved1?, reserved2?)TypedArray.prototype.toString()TypedArray.prototype.values()
复合视图:
由于视图的构造函数可以指定起始位置和长度,所以在同一段内存之中,可以依次存放不同类型的数据,这叫做"复合视图"。
-
const buffer = new ArrayBuffer(24); -
const idView = new Uint32Array(buffer, 0, 1); -
const usernameView = new Uint8Array(buffer, 4, 16); -
const amountDueView = new Float32Array(buffer, 20, 1);
DataView视图本身也是构造函数,接受一个ArrayBuffer对象作为参数,生成视图。
new DataView(ArrayBuffer buffer [, 字节起始位置 [, 长度]]);
下面是一个例子。
const buffer = new ArrayBuffer(24);const dv = new DataView(buffer);
DataView实例有以下属性,含义与TypedArray实例的同名方法相同。
DataView.prototype.buffer:返回对应的 ArrayBuffer 对象DataView.prototype.byteLength:返回占据的内存字节长度DataView.prototype.byteOffset:返回当前视图从对应的 ArrayBuffer 对象的哪个字节开始
DataView实例提供 8 个方法读取内存。
getInt8:读取 1 个字节,返回一个 8 位整数。getUint8:读取 1 个字节,返回一个无符号的 8 位整数。getInt16:读取 2 个字节,返回一个 16 位整数。getUint16:读取 2 个字节,返回一个无符号的 16 位整数。getInt32:读取 4 个字节,返回一个 32 位整数。getUint32:读取 4 个字节,返回一个无符号的 32 位整数。getFloat32:读取 4 个字节,返回一个 32 位浮点数。getFloat64:读取 8 个字节,返回一个 64 位浮点数。
arraybuffer的应用:
传统上,服务器通过 AJAX 操作只能返回文本数据,即responseType属性默认为text。XMLHttpRequest第二版XHR2允许服务器返回二进制数据,这时分成两种情况。如果明确知道返回的二进制数据类型,可以把返回类型(responseType)设为arraybuffer;如果不知道,就设为blob。
-
let xhr = new XMLHttpRequest(); -
xhr.open('GET', someUrl); -
xhr.responseType = 'arraybuffer'; -
xhr.onload = function () { -
let arrayBuffer = xhr.response; -
// ··· -
}; -
xhr.send();
网页Canvas元素输出的二进制像素数据,就是 TypedArray 数组。
-
const canvas = document.getElementById('myCanvas'); -
const ctx = canvas.getContext('2d'); -
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); -
const uint8ClampedArray = imageData.data;
ES2017 引入SharedArrayBuffer,允许 Worker 线程与主线程共享同一块内存。SharedArrayBuffer的 API 与ArrayBuffer一模一样,唯一的区别是后者无法共享数据。
-
// 主线程 -
// 新建 1KB 共享内存 -
const sharedBuffer = new SharedArrayBuffer(1024); -
// 主线程将共享内存的地址发送出去 -
w.postMessage(sharedBuffer); -
// 在共享内存上建立视图,供写入数据 -
const sharedArray = new Int32Array(sharedBuffer);
上面代码中,postMessage方法的参数是SharedArrayBuffer对象。
Worker 线程从事件的data属性上面取到数据。
-
// Worker 线程 -
onmessage = function (ev) { -
// 主线程共享的数据,就是 1KB 的共享内存 -
const sharedBuffer = ev.data; -
// 在共享内存上建立视图,方便读写 -
const sharedArray = new Int32Array(sharedBuffer); -
// ... -
};
共享内存也可以在 Worker 线程创建,发给主线程。
SharedArrayBuffer与ArrayBuffer一样,本身是无法读写的,必须在上面建立视图,然后通过视图读写。
-
// 分配 10 万个 32 位整数占据的内存空间 -
const sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 100000); -
// 建立 32 位整数视图 -
const ia = new Int32Array(sab); // ia.length == 100000 -
// 新建一个质数生成器 -
const primes = new PrimeGenerator(); -
// 将 10 万个质数,写入这段内存空间 -
for ( let i=0 ; i < ia.length ; i++ ) -
ia[i] = primes.next(); -
// 向 Worker 线程发送这段共享内存 -
w.postMessage(ia);
Worker 线程收到数据后的处理如下。
// Worker 线程let ia;onmessage = function (ev) {ia = ev.data;console.log(ia.length); // 100000console.log(ia[37]); // 输出 163,因为这是第38个质数};
多线程共享内存,最大的问题就是如何防止两个线程同时修改某个地址,或者说,当一个线程修改共享内存以后,必须有一个机制让其他线程同步。SharedArrayBuffer API 提供Atomics对象,保证所有共享内存的操作都是"原子性"的,并且可以在所有线程内同步。
共享内存上面的某些运算是不能被打断的,即不能在运算过程中,让其他线程改写内存上面的值。Atomics 对象提供了一些运算方法,防止数据被改写。
Atomics.add(sharedArray, index, value)
Atomics.add用于将value加到sharedArray[index],返回sharedArray[index]旧的值。
Atomics.sub(sharedArray, index, value)
Atomics.sub用于将value从sharedArray[index]减去,返回sharedArray[index]旧的值。
Atomics.and(sharedArray, index, value)
Atomics.and用于将value与sharedArray[index]进行位运算and,放入sharedArray[index],并返回旧的值。
Atomics.or(sharedArray, index, value)
Atomics.or用于将value与sharedArray[index]进行位运算or,放入sharedArray[index],并返回旧的值。
Atomics.xor(sharedArray, index, value)
Atomic.xor用于将vaule与sharedArray[index]进行位运算xor,放入sharedArray[index],并返回旧的值。
(5)其他方法
Atomics对象还有以下方法。
Atomics.compareExchange(sharedArray, index, oldval, newval):如果sharedArray[index]等于oldval,就写入newval,返回oldval。Atomics.isLockFree(size):返回一个布尔值,表示Atomics对象是否可以处理某个size的内存锁定。如果返回false,应用程序就需要自己来实现锁定。
Atomics.compareExchange的一个用途是,从 SharedArrayBuffer 读取一个值,然后对该值进行某个操作,操作结束以后,检查一下 SharedArrayBuffer 里面原来那个值是否发生变化(即被其他线程改写过)。如果没有改写过,就将它写回原来的位置,否则读取新的值,再重头进行一次操作。
29. 最新提案
do 表达式
-
// 等同于 <表达式> -
do { <表达式>; } -
// 等同于 <语句> -
do { <语句> }
do表达式的好处是可以封装多个语句,让程序更加模块化,就像乐高积木那样一块块拼装起来。
let x = do {if (foo()) { f() }else if (bar()) { g() }else { h() }};
开发者使用一个模块时,有时需要知道模板本身的一些信息(比如模块的路径)。现在有一个提案,为 import 命令添加了一个元属性import.meta,返回当前模块的元信息。
import.meta只能在模块内部使用,如果在模块外部使用会报错。
这个属性返回一个对象,该对象的各种属性就是当前运行的脚本的元信息。具体包含哪些属性,标准没有规定,由各个运行环境自行决定。一般来说,import.meta至少会有下面两个属性。
(1)import.meta.url
import.meta.url返回当前模块的 URL 路径。举例来说,当前模块主文件的路径是https://foo.com/main.js,import.meta.url就返回这个路径。如果模块里面还有一个数据文件data.txt,那么就可以用下面的代码,获取这个数据文件的路径。
new URL('data.txt', import.meta.url)
注意,Node.js 环境中,import.meta.url返回的总是本地路径,即是file:URL协议的字符串,比如file:///home/user/foo.js。
(2)import.meta.scriptElement
import.meta.scriptElement是浏览器特有的元属性,返回加载模块的那个<script>元素,相当于document.currentScript属性。
-
// HTML 代码为 -
// <script type="module" src="my-module.js" data-foo="abc"></script> -
// my-module.js 内部执行下面的代码 -
import.meta.scriptElement.dataset.foo -
// "abc"
函数的部分执行有一些特别注意的地方。
(1)函数的部分执行是基于原函数的。如果原函数发生变化,部分执行生成的新函数也会立即反映这种变化。
(2)如果预先提供的那个值是一个表达式,那么这个表达式并不会在定义时求值,而是在每次调用时求值。
(3)如果新函数的参数多于占位符的数量,那么多余的参数将被忽略。
(4)...只会被采集一次,如果函数的部分执行使用了多个...,那么每个...的值都将相同。
JavaScript 的管道是一个运算符,写作|>。它的左边是一个表达式,右边是一个函数。管道运算符把左边表达式的值,传入右边的函数进行求值。
x |> f// 等同于f(x)
数值分割:
-
123_00 === 12_300 // true -
12345_00 === 123_4500 // true -
12345_00 === 1_234_500 // true
数值分隔符有几个使用注意点。
- 不能在数值的最前面(leading)或最后面(trailing)。
- 不能两个或两个以上的分隔符连在一起。
- 小数点的前后不能有分隔符。
- 科学计数法里面,表示指数的
e或E前后不能有分隔符。
Math.sign()用来判断一个值的正负,但是如果参数是-0,它会返回-0。
-
Math.sign(-0) // -0 -
Math.signbit(2) //false -
Math.signbit(-2) //true -
Math.signbit(0) //false -
Math.signbit(-0) //true
双冒号运算符
箭头函数可以绑定this对象,大大减少了显式绑定this对象的写法(call、apply、bind)。但是,箭头函数并不适用于所有场合,所以现在有一个提案,提出了"函数绑定"(function bind)运算符,用来取代call、apply、bind调用。
函数绑定运算符是并排的两个冒号(::),双冒号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象,作为上下文环境(即this对象),绑定到右边的函数上面。
-
foo::bar; -
// 等同于 -
bar.bind(foo); -
foo::bar(...arguments); -
// 等同于 -
bar.apply(foo, arguments); -
const hasOwnProperty = Object.prototype.hasOwnProperty; -
function hasOwn(obj, key) { -
return obj::hasOwnProperty(key); -
}
如果双冒号左边为空,右边是一个对象的方法,则等于将该方法绑定在该对象上面。
-
var method = obj::obj.foo; -
// 等同于 -
var method = ::obj.foo; -
let log = ::console.log; -
// 等同于 -
var log = console.log.bind(console);
如果双冒号运算符的运算结果,还是一个对象,就可以采用链式写法。
-
import { map, takeWhile, forEach } from "iterlib"; -
getPlayers() -
::map(x => x.character()) -
::takeWhile(x => x.strength > 100) -
::forEach(x => console.log(x));
30. Decorator
装饰器可以用来装饰整个类。
-
function testable(isTestable) { -
return function(target) {
`target.isTestable = isTestable;`
-
} -
} -
@testable(true) -
class MyTestableClass {} -
MyTestableClass.isTestable // true -
@testable(false) -
class MyClass {} -
MyClass.isTestable // false
上面代码中,@testable就是一个装饰器。它修改了MyTestableClass这个类的行为,为它加上了静态属性isTestable。testable函数的参数target是MyTestableClass类本身。
装饰器不仅可以装饰类,还可以装饰类的属性。
class Person {@readonlyname() { return `${this.first} ${this.last}` }}
上面代码中,装饰器readonly用来装饰"类"的name方法。
装饰器函数readonly一共可以接受三个参数。
-
function readonly(target, name, descriptor){ -
// descriptor对象原来的值如下 -
// { -
// value: specifiedFunction, -
// enumerable: false, -
// configurable: true, -
// writable: true -
// }; -
descriptor.writable = false; -
return descriptor; -
} -
readonly(Person.prototype, 'name', descriptor); -
// 类似于 -
Object.defineProperty(Person.prototype, 'name', descriptor);
装饰器第一个参数是类的原型对象,上例是Person.prototype,装饰器的本意是要"装饰"类的实例,但是这个时候实例还没生成,所以只能去装饰原型(这不同于类的装饰,那种情况时target参数指的是类本身);第二个参数是所要装饰的属性名,第三个参数是该属性的描述对象。
另外,上面代码说明,装饰器(readonly)会修改属性的描述对象(descriptor),然后被修改的描述对象再用来定义属性。
装饰器只适用于类和类的方法,并不适用于函数
core-decorators.js
core-decorators.js是一个第三方模块,提供了几个常见的装饰器,通过它可以更好地理解装饰器。
autobind装饰器使得方法中的this对象,绑定原始对象。
readonly装饰器使得属性或方法不可写。
override装饰器检查子类的方法,是否正确覆盖了父类的同名方法,如果不正确会报错。
deprecate或deprecated装饰器在控制台显示一条警告,表示该方法将废除。
suppressWarnings装饰器抑制deprecated装饰器导致的console.warn()调用。但是,异步代码发出的调用除外。
在装饰器的基础上,可以实现Mixin模式。所谓Mixin模式,就是对象继承的一种替代方案,中文译为"混入"(mix in),意为在一个对象之中混入另外一个对象的方法。
方法一:
-
const Foo = { -
foo() { console.log('foo') } -
}; -
class MyClass {} -
Object.assign(MyClass.prototype, Foo); -
let obj = new MyClass(); -
obj.foo() // 'foo'
方法二:
部署一个通用脚本mixins.js,将 Mixin 写成一个装饰器。
export function mixins(...list) {return function (target) {
`Object.assign(target.prototype, ...list);`
};}
然后,就可以使用上面这个装饰器,为类"混入"各种方法。
-
import { mixins } from './mixins'; -
const Foo = { -
foo() { console.log('foo') } -
}; -
@mixins(Foo) -
class MyClass {} -
let obj = new MyClass(); -
obj.foo() // "foo"
Trait 也是一种装饰器,效果与 Mixin 类似,但是提供更多功能,比如防止同名方法的冲突、排除混入某些方法、为混入的方法起别名等等。
下面采用traits-decorator这个第三方模块作为例子。这个模块提供的traits装饰器,不仅可以接受对象,还可以接受 ES6 类作为参数。
-
import { traits } from 'traits-decorator'; -
class TFoo { -
foo() { console.log('foo') } -
} -
const TBar = { -
bar() { console.log('bar') } -
}; -
@traits(TFoo, TBar) -
class MyClass { } -
let obj = new MyClass(); -
obj.foo() // foo -
obj.bar() // bar
31. 参考链接
官方文件
- ECMAScript® 2015 Language Specification: ECMAScript 2015 规格
- ECMAScript® 2016 Language Specification: ECMAScript 2016 规格
- ECMAScript® 2017 Language Specification:ECMAScript 2017 规格(草案)
- ECMAScript Current Proposals: ECMAScript 当前的所有提案
- ECMAScript Active Proposals: 已经进入正式流程的提案
- ECMAScript proposals:从阶段 0 到阶段 4 的所有提案列表
- TC39 meeting agendas: TC39 委员会历年的会议记录
- ECMAScript Daily: TC39 委员会的动态
- The TC39 Process: 提案进入正式规格的流程
- TC39: A Process Sketch, Stages 0 and 1: Stage 0 和 Stage 1 的含义
- TC39 Process Sketch, Stage 2: Stage 2 的含义
综合介绍
- Axel Rauschmayer, Exploring ES6: Upgrade to the next version of JavaScript: ES6 的专著,本书的许多代码实例来自该书
- Sayanee Basu, Use ECMAScript 6 Today
- Ariya Hidayat, Toward Modern Web Apps with ECMAScript 6
- Dale Schouten, 10 Ecmascript-6 tricks you can perform right now
- Colin Toh, Lightweight ES6 Features That Pack A Punch: ES6 的一些"轻量级"的特性介绍
- Domenic Denicola, ES6: The Awesome Parts
- Nicholas C. Zakas, Understanding ECMAScript 6
- Justin Drake, ECMAScript 6 in Node.JS
- Ryan Dao, Summary of ECMAScript 6 major features
- Luke Hoban, ES6 features: ES6 新语法点的罗列
- Traceur-compiler, Language Features: Traceur 文档列出的一些 ES6 例子
- Axel Rauschmayer, ECMAScript 6: what's next for JavaScript?: 关于 ES6 新增语法的综合介绍,有很多例子
- Axel Rauschmayer, Getting started with ECMAScript 6: ES6 语法点的综合介绍
- Toby Ho, ES6 in io.js
- Guillermo Rauch, ECMAScript 6
- Benjamin De Cock, Frontend Guidelines: ES6 最佳实践
- Jani Hartikainen, ES6: What are the benefits of the new features in practice?
- kangax, JavaScript quiz. ES6 edition: ES6 小测试
- Jeremy Fairbank, HTML5DevConf ES7 and Beyond!: ES7 新增语法点介绍
- Timothy Gu, How to Read the ECMAScript Specification: 如何读懂 ES6 规格
let 和 const
- Kyle Simpson, For and against let: 讨论 let 命令的作用域
- kangax, Why typeof is no longer "safe": 讨论在块级作用域内,let 命令的变量声明和赋值的行为
- Axel Rauschmayer, Variables and scoping in ECMAScript 6: 讨论块级作用域与 let 和 const 的行为
- Nicolas Bevacqua, ES6 Let, Const and the "Temporal Dead Zone" (TDZ) in Depth
- acorn, Function statements in strict mode: 块级作用域对严格模式的函数声明的影响
- Axel Rauschmayer, ES proposal: global: 顶层对象
global - Mathias Bynens, A horrifying
globalThispolyfill in universal JavaScript:如何写 globalThis 的垫片库
解构赋值
- Nick Fitzgerald, Destructuring Assignment in ECMAScript 6: 详细介绍解构赋值的用法
- Nicholas C. Zakas, ECMAScript 6 destructuring gotcha
字符串
- Nicholas C. Zakas, A critical review of ECMAScript 6 quasi-literals
- Mozilla Developer Network, Template strings
- Addy Osmani, Getting Literal With ES6 Template Strings: 模板字符串的介绍
- Blake Winton, ES6 Templates: 模板字符串的介绍
- Peter Jaszkowiak, How to write a template compiler in JavaScript: 使用模板字符串,编写一个模板编译函数
- Axel Rauschmayer, ES.stage3: string padding
正则
- Mathias Bynens, Unicode-aware regular expressions in ES6: 详细介绍正则表达式的 u 修饰符
- Axel Rauschmayer, New regular expression features in ECMAScript 6:ES6 正则特性的详细介绍
- Yang Guo, RegExp lookbehind assertions:介绍后行断言
- Axel Rauschmayer, ES proposal: RegExp named capture groups: 具名组匹配的介绍
- Mathias Bynens, ECMAScript regular expressions are getting better!: 介绍 ES2018 添加的多项正则语法
数值
- Nicolas Bevacqua, ES6 Number Improvements in Depth
- Axel Rauschmayer, ES proposal: arbitrary precision integers
- Mathias Bynens, BigInt: arbitrary-precision integers in JavaScript
数组
- Axel Rauschmayer, ECMAScript 6's new array methods: 对 ES6 新增的数组方法的全面介绍
- TC39, Array.prototype.includes: 数组的 includes 方法的规格
- Axel Rauschmayer, ECMAScript 6: holes in Arrays: 数组的空位问题
函数
- Nicholas C. Zakas, Understanding ECMAScript 6 arrow functions
- Jack Franklin, Real Life ES6 - Arrow Functions
- Axel Rauschmayer, Handling required parameters in ECMAScript 6
- Dmitry Soshnikov, ES6 Notes: Default values of parameters: 介绍参数的默认值
- Ragan Wald, Destructuring and Recursion in ES6: rest 参数和扩展运算符的详细介绍
- Axel Rauschmayer, The names of functions in ES6: 函数的 name 属性的详细介绍
- Kyle Simpson, Arrow This: 箭头函数并没有自己的 this
- Derick Bailey, Do ES6 Arrow Functions Really Solve "this" In JavaScript?:使用箭头函数处理 this 指向,必须非常小心
- Mark McDonnell, Understanding recursion in functional JavaScript programming: 如何自己实现尾递归优化
- Nicholas C. Zakas, The ECMAScript 2016 change you probably don't know: 使用参数默认值时,不能在函数内部显式开启严格模式
- Axel Rauschmayer, ES proposal: optional catch binding
- Cynthia Lee, When you should use ES6 arrow functions --- and when you shouldn't: 讨论箭头函数的适用场合
- Eric Elliott, What is this?: 箭头函数内部的 this 的解释。
对象
- Addy Osmani, Data-binding Revolutions with Object.observe(): 介绍 Object.observe()的概念
- Sella Rafaeli, Native JavaScript Data-Binding: 如何使用 Object.observe 方法,实现数据对象与 DOM 对象的双向绑定
- Axel Rauschmayer,
__proto__in ECMAScript 6 - Axel Rauschmayer, Enumerability in ECMAScript 6
- Axel Rauschmayer, ES proposal: Object.getOwnPropertyDescriptors()
- TC39, Object.getOwnPropertyDescriptors Proposal
- David Titarenco, How Spread Syntax Breaks JavaScript: 扩展运算符的一些不合理的地方
Symbol
- Axel Rauschmayer, Symbols in ECMAScript 6: Symbol 简介
- MDN, Symbol: Symbol 类型的详细介绍
- Jason Orendorff, ES6 In Depth: Symbols
- Keith Cirkel, Metaprogramming in ES6: Symbols and why they're awesome: Symbol 的深入介绍
- Axel Rauschmayer, Customizing ES6 via well-known symbols
- Derick Bailey, Creating A True Singleton In Node.js, With ES6 Symbols
- Das Surma, How to read web specs Part IIa -- Or: ECMAScript Symbols: 介绍 Symbol 的规格
Set 和 Map
- Mozilla Developer Network, WeakSet:介绍 WeakSet 数据结构
- Dwayne Charrington, What Are Weakmaps In ES6?: WeakMap 数据结构介绍
- Axel Rauschmayer, ECMAScript 6: maps and sets: Set 和 Map 结构的详细介绍
- Jason Orendorff, ES6 In Depth: Collections:Set 和 Map 结构的设计思想
- Axel Rauschmayer, Converting ES6 Maps to and from JSON: 如何将 Map 与其他数据结构互相转换
Proxy 和 Reflect
- Nicholas C. Zakas, Creating defensive objects with ES6 proxies
- Axel Rauschmayer, Meta programming with ECMAScript 6 proxies: Proxy 详解
- Daniel Zautner, Meta-programming JavaScript Using Proxies: 使用 Proxy 实现元编程
- Tom Van Cutsem, Harmony-reflect: Reflect 对象的设计目的
- Tom Van Cutsem, Proxy Traps: Proxy 拦截操作一览
- Tom Van Cutsem, Reflect API
- Tom Van Cutsem, Proxy Handler API
- Nicolas Bevacqua, ES6 Proxies in Depth
- Nicolas Bevacqua, ES6 Proxy Traps in Depth
- Nicolas Bevacqua, More ES6 Proxy Traps in Depth
- Axel Rauschmayer, Pitfall: not all objects can be wrapped transparently by proxies
- Bertalan Miklos, Writing a JavaScript Framework - Data Binding with ES6 Proxies: 使用 Proxy 实现观察者模式
- Keith Cirkel, Metaprogramming in ES6: Part 2 - Reflect: Reflect API 的详细介绍
Promise 对象
- Jake Archibald, JavaScript Promises: There and back again
- Jake Archibald, Tasks, microtasks, queues and schedules
- Tilde, rsvp.js
- Sandeep Panda, An Overview of JavaScript Promises: ES6 Promise 入门介绍
- Dave Atchley, ES6 Promises: Promise 的语法介绍
- Axel Rauschmayer, ECMAScript 6 promises (2/2): the API: 对 ES6 Promise 规格和用法的详细介绍
- Jack Franklin, Embracing Promises in JavaScript: catch 方法的例子
- Ronald Chen, How to escape Promise Hell: 如何使用
Promise.all方法的一些很好的例子 - Jordan Harband, proposal-promise-try: Promise.try() 方法的提案
- Sven Slootweg, What is Promise.try, and why does it matter?: Promise.try() 方法的优点
- Yehuda Katz, TC39: Promises, Promises: Promise.try() 的用处
Iterator
- Mozilla Developer Network, Iterators and generators
- Mozilla Developer Network, The Iterator protocol
- Jason Orendorff, ES6 In Depth: Iterators and the for-of loop: 遍历器与 for...of 循环的介绍
- Axel Rauschmayer, Iterators and generators in ECMAScript 6: 探讨 Iterator 和 Generator 的设计目的
- Axel Rauschmayer, Iterables and iterators in ECMAScript 6: Iterator 的详细介绍
- Kyle Simpson, Iterating ES6 Numbers: 在数值对象上部署遍历器
Generator
- Matt Baker, Replacing callbacks with ES6 Generators
- Steven Sanderson, Experiments with Koa and JavaScript Generators
- jmar777, What's the Big Deal with Generators?
- Marc Harter, Generators in Node.js: Common Misconceptions and Three Good Use Cases: 讨论 Generator 函数的作用
- StackOverflow, ES6 yield : what happens to the arguments of the first call next()?: 第一次使用 next 方法时不能带有参数
- Kyle Simpson, ES6 Generators: Complete Series: 由浅入深探讨 Generator 的系列文章,共四篇
- Gajus Kuizinas, The Definitive Guide to the JavaScript Generators: 对 Generator 的综合介绍
- Jan Krems, Generators Are Like Arrays: 讨论 Generator 可以被当作数据结构看待
- Harold Cooper, Coroutine Event Loops in JavaScript: Generator 用于实现状态机
- Ruslan Ismagilov, learn-generators: 编程练习,共 6 道题
- Steven Sanderson, Experiments with Koa and JavaScript Generators: Generator 入门介绍,以 Koa 框架为例
- Mahdi Dibaiee, ES7 Array and Generator comprehensions:ES7 的 Generator 推导
- Nicolas Bevacqua, ES6 Generators in Depth
- Axel Rauschmayer, ES6 generators in depth: Generator 规格的详尽讲解
- Derick Bailey, Using ES6 Generators To Short-Circuit Hierarchical Data Iteration:使用 for...of 循环完成预定的操作步骤
异步操作和 Async 函数
- Luke Hoban, Async Functions for ECMAScript: Async 函数的设计思想,与 Promise、Gernerator 函数的关系
- Jafar Husain, Asynchronous Generators for ES7: Async 函数的深入讨论
- Nolan Lawson, Taming the asynchronous beast with ES7: async 函数通俗的实例讲解
- Jafar Husain, Async Generators: 对 async 与 Generator 混合使用的一些讨论
- Daniel Brain, Understand promises before you start using async/await: 讨论 async/await 与 Promise 的关系
- Jake Archibald, Async functions - making promises friendly
- Axel Rauschmayer, ES proposal: asynchronous iteration: 异步遍历器的详细介绍
- Dima Grossman, How to write async await without try-catch blocks in JavaScript: 除了 try/catch 以外的 async 函数内部捕捉错误的方法
- Mostafa Gaafa, 6 Reasons Why JavaScript's Async/Await Blows Promises Away: Async 函数的6个好处
- Mathias Bynens, Asynchronous stack traces: why await beats Promise#then(): async 函数可以保留错误堆栈
Class
- Sebastian Porto, ES6 classes and JavaScript prototypes: ES6 Class 的写法与 ES5 Prototype 的写法对比
- Jack Franklin, An introduction to ES6 classes: ES6 class 的入门介绍
- Axel Rauschmayer, ECMAScript 6: new OOP features besides classes
- Axel Rauschmayer, Classes in ECMAScript 6 (final semantics): Class 语法的详细介绍和设计思想分析
- Eric Faust, ES6 In Depth: Subclassing: Class 语法的深入介绍
- Nicolás Bevacqua, Binding Methods to Class Instance Objects: 如何绑定类的实例中的 this
- Jamie Kyle, JavaScript's new #private class fields:私有属性的介绍。
- Mathias Bynens, Public and private class fields:实例属性的新写法的介绍。
Decorator
- Maximiliano Fierro, Declarative vs Imperative: Decorators 和 Mixin 介绍
- Justin Fagnani, "Real" Mixins with JavaScript Classes: 使用类的继承实现 Mixin
- Addy Osmani, Exploring ES2016 Decorators: Decorator 的深入介绍
- Sebastian McKenzie, Allow decorators for functions as well: 为什么修饰器不能用于函数
- Maximiliano Fierro, Traits with ES7 Decorators: Trait 的用法介绍
- Jonathan Creamer: Using ES2016 Decorators to Publish on an Event Bus: 使用修饰器实现自动发布事件
Module
- Jack Franklin, JavaScript Modules the ES6 Way: ES6 模块入门
- Axel Rauschmayer, ECMAScript 6 modules: the final syntax: ES6 模块的介绍,以及与 CommonJS 规格的详细比较
- Dave Herman, Static module resolution: ES6 模块的静态化设计思想
- Jason Orendorff, ES6 In Depth: Modules: ES6 模块设计思想的介绍
- Ben Newman, The Importance of import and export: ES6 模块的设计思想
- ESDiscuss, Why is "export default var a = 1;" invalid syntax?
- Bradley Meck, ES6 Module Interoperability: 介绍 Node 如何处理 ES6 语法加载 CommonJS 模块
- Axel Rauschmayer, Making transpiled ES modules more spec-compliant: ES6 模块编译成 CommonJS 模块的详细介绍
- Axel Rauschmayer, ES proposal: import() -- dynamically importing ES modules: import() 的用法
- Node EPS, ES Module Interoperability: Node 对 ES6 模块的处理规格
二进制数组
- Ilmari Heikkinen, Typed Arrays: Binary Data in the Browser
- Khronos, Typed Array Specification
- Ian Elliot, Reading A BMP File In JavaScript
- Renato Mangini, How to convert ArrayBuffer to and from String
- Axel Rauschmayer, Typed Arrays in ECMAScript 6
- Axel Rauschmayer, ES proposal: Shared memory and atomics
- Lin Clark, Avoiding race conditions in SharedArrayBuffers with Atomics: Atomics 对象使用场景的解释
- Lars T Hansen, Shared memory - a brief tutorial
- James Milner, The Return of SharedArrayBuffers and Atomics
SIMD
- TC39, SIMD.js Stage 2
- MDN, SIMD
- TC39, ECMAScript SIMD
- Axel Rauschmayer, JavaScript gains support for SIMD
工具
- Babel, Babel Handbook: Babel 的用法介绍
- Google, traceur-compiler: Traceur 编译器
- Casper Beyer, ECMAScript 6 Features and Tools
- Stoyan Stefanov, Writing ES6 today with jstransform
- ES6 Module Loader, ES6 Module Loader Polyfill: 在浏览器和 node.js 加载 ES6 模块的一个库,文档里对 ES6 模块有详细解释
- Paul Miller, es6-shim: 一个针对老式浏览器,模拟 ES6 部分功能的垫片库(shim)
- army8735, JavaScript Downcast: 国产的 ES6 到 ES5 的转码器
- esnext, ES6 Module Transpiler:基于 node.js 的将 ES6 模块转为 ES5 代码的命令行工具
- Sebastian McKenzie, BabelJS: ES6 转译器
- SystemJS, SystemJS: 在浏览器中加载 AMD、CJS、ES6 模块的一个垫片库
- Modernizr, HTML5 Cross Browser Polyfills: ES6 垫片库清单
- Facebook, regenerator: 将 Generator 函数转为 ES5 的转码器
32. Mixin
JavaScript 语言的设计是单一继承,即子类只能继承一个父类,不允许继承多个父类。这种设计保证了对象继承的层次结构是树状的,而不是复杂的网状结构。
但是,这大大降低了编程的灵活性。因为实际开发中,有时不可避免,子类需要继承多个父类。举例来说,"猫"可以继承"哺乳类动物",也可以继承"宠物"。
这里使用mixin和trait解决
33. SIMD
SIMD(发音/sim-dee/)是"Single Instruction/Multiple Data"的缩写,意为"单指令,多数据"。它是 JavaScript 操作 CPU 对应指令的接口,你可以看做这是一种不同的运算执行模式。与它相对的是 SISD("Single Instruction/Single Data"),即"单指令,单数据"。
SIMD 的含义是使用一个指令,完成多个数据的运算;SISD 的含义是使用一个指令,完成单个数据的运算,这是 JavaScript 的默认运算模式。显而易见,SIMD 的执行效率要高于 SISD,所以被广泛用于 3D 图形运算、物理模拟等运算量超大的项目之中。
总的来说,SIMD 是数据并行处理(parallelism)的一种手段,可以加速一些运算密集型操作的速度。将来与 WebAssembly 结合以后,可以让 JavaScript 达到二进制代码的运行速度。
34. 函数式编程
柯里化(currying)指的是将一个多参数的函数拆分成一系列函数,每个拆分后的函数都只接受一个参数(unary)。
-
function add (a) { -
return function (b) {
`return a + b;`
-
} -
} -
// 或者采用箭头函数写法 -
const add = x => y => x + y; -
const f = add(1); -
f(1) // 2
函数合成(function composition)指的是,将多个函数合成一个函数。
-
const compose = f => g => x => f(g(x)); -
const f = compose (x => x * 4) (x => x + 3); -
f(2) // 20
参数倒置(flip)指的是改变函数前两个参数的顺序。
-
let f = {}; -
f.flip = -
fn =>
`(a, b, ...args) => fn(b, a, ...args.reverse());`
-
var divide = (a, b) => a / b; -
var flip = f.flip(divide); -
flip(10, 5) // 0.5 -
flip(1, 10) // 10 -
var three = (a, b, c) => [a, b, c]; -
var flip = f.flip(three); -
flip(1, 2, 3); // => [2, 1, 3]
执行边界(until)指的是函数执行到满足条件为止。
-
let f = {}; -
f.until = (condition, f) => -
(...args) => {
`var r = f.apply(null, args);`
`return condition(r) ? r : f.until(condition, f)(r);`
-
}; -
let condition = x => x > 100; -
let inc = x => x + 1; -
let until = f.until(condition, inc); -
until(0) // 101 -
condition = x => x === 5; -
until = f.until(condition, inc); -
until(3) // 5
Mateo Gianolio, Haskell in ES6: Part 1
next()、throw()、return()这三个方法本质上是同一件事,可以放在一起理解。它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换yield表达式。