序:
1.用let const 声明变量。
2.解构赋值:
用途:a.交换变量的值; b.从函数返回多个值; c.函数参数的定义及默认值; d.提取JSON数据; e.遍历Map; f.输入模块。
3.字符串的扩展:
a.完善以前超出范围的字符处理; b.可以用for...of循环; c.includes(),startWith(),endsWith(),repeat(),
padStart(),padEnd()等api; d.模板字符串; e.标签模板(是函数调用的一种特殊形式。“标签”指的就是函数,紧跟在后面的模板字符串就是它的参数。作用1:过滤html字符串,防止用户恶意输入;2.多语言转换。)
4.正则的扩展:
u 修饰符(完善以前超出范围的字符处理)、y修饰符(粘连;类似g修饰符,确保匹配必须从剩余的第一个位置开始。)等。
5.数值的扩展:
Number.isNaN() Number.isFinite()(只对数值有效,不转换) Number.isInteger()(是否为整数) Number.EPSILON()(设置误差范围 比如0.1+0.2不等于0.3) 。Math.trunc()(去除小数部分);Math.sign()(判断一个数是正负还是0或NAN);Math.cbrt()(立方根); Math.clz32(); Math.imul();Math.fround();Math.hypot()(所有参数的平方和的平方根) 还有一些对数方法及指数运算符(**)等。
6.函数的扩展:
a参数可以设置默认值; b.rest参数(...变量名,是一个数组。),用于获取函数的多余参数,可以取代arguments对象。 c.箭头函数(this是定义时的指向,不是运行时指向。无arguments对象,不能new,不能yield); d.双冒号运算符(::用于绑定this); e.尾调用和尾递归的优化(减少调用帧,节省内存。可以用循环代替递归)。
7.数组的扩展:
a.扩展运算符(...)是rest参数的逆运算,将数组转为逗号分隔的参数序列,主要用于函数调用,可替代apply方法,用途很广;也可以将某些数据结构转为数组(背后调用的是iterator接口); b.Array.from:将类数组对象和可遍历对象转为真正的数组。(只要有length属性都可以转换); c.Array.of():将一组值,转换为数组; d.copyWithin()(将指定位置的成员复制到其他位置,会覆盖,然后返回当前数组); e.find()和findIndex()(参数是一个回调函数,返回第一个找到的成员(下标)) ; f.fill():填充数组,用于初始化数组; g.keys(),values() entries(),:分别遍历下标,键值,键值对。 h.includes():类似字符串的includes。 i.数组的空位(尽量避免空位)
8.对象的扩展:
a.简洁表达法; b.属性名表达式([property]) c.Object.is()(与===的区别为is的+0不等于 -0,NaN等于自身); d.Object.assign()(用于合并对象,同名属性会覆盖,属于浅拷贝,即拷贝的是引用。),可以为对象添加属性、方法、克隆对象、合并对象、为属性指定默认值等。 e.Object.getOwnPropertyDescriptor(s)获取属性的描述对象
f.属性的遍历(for...in(遍历对象自身和继承的可枚举属性) Object.keys(obj)(返回一个数组,包括对象自身(不含继承的)可枚举属性) Object.getOwnPropertyNames(obj) (返回一个数组,包含对象自身的所有属性,包括不可枚举的)Object.getOwnPropertySymbols(obj) Reflect.ownKeys(obj)(包含所有键名,包含Symbol,也无论是否可枚举)
g.__proto__属性的替代:Object.setPrototypeOf()、Object.getPrototypeOf()、Object.create();
h.super()关键字:指向对象的原型对象。
i.Object.keys():返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名。
Object.values():方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值。
Object.entries():方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对数组。可用于遍历对象的属性,和讲对象转为Map结构。
j. 对象的扩展运算符; k. ?. (简化判断是否存在的写法)。
Symbol:
独一无二的值,通过Symbol函数生成;如Symbol.iterator属性
Set和Map结构:
1.Set:
a.类似于数组,但是成员的值都是唯一的,没有重复的值,可以去重。 b.属性:constructor,size;
c.方法:add(value),delete(value),has(value),clear(); d.遍历:keys(),values(),entries(),forEach();
WeakSet适合临时存放一组对象,不适合引用,因为会随时消失;
2.Map:
a.提供(值-值对结构),可以被for...of遍历; b.属性和方法:size, set(key,value), get(key), has(key) ,delete(key),
clear(); c.遍历:同set;
WeakMap 可以将dom节点作为键名,有助于防止内存泄露。
Proxy:可以在目标对象之前设置一层拦截,对外界的访问进行过滤和改写。
Reflect:未来从Reflect身上拿语言内部的方法并修正细节。如Object.defineProperty等
Promise对象:
异步回调解决的一种方案,Promise实例生成后,可以用then方法分别指定resolved和rejected状态的回调函数。Promise.all()将多个Promise实例包装成一个实例。都完成才完成,一个失败即失败;Promise.race()只要有一个实例率先改变状态,总实例就改变状态。Promise也可以结合Generator函数使用。
Iterator和for...of:
a.可以为数据(Object Array Map Set)设置Iterator接口(Symbol.iterator),可以供for...of遍历。
b.调用Iterator接口的场合:解构,...运算符,yield* 数组,字符串,for...of,Generator,Set,Map,类数组也可for...of;
普通对象需要部署接口或者用Object.keys()或Generator函数重新包装。
c.for...of的优势:1.没有for..in的一些缺点(字符串下标,牵扯原型链等);2.与forEach比可以与break,continue,return配合;3.提供遍历数据统一接口。
Generator:
a.异步编程解决方案之一,状态机及遍历器对象生成函数。调用后不执行,返回指向内部状态的遍历器对象。yield表达式是暂停执行的标记,调用next方法,移动指针至下一步,直到结束或return。
b.next方法可以带参数,该参数被当做上一个yield表达式的返回值(否则yield表达式为undefined)
c.可以将generator函数改造成普通构造函数。
d.generator的异步应用:需配合Thunkify或co模块。
e.generator函数的语法糖升级版:async函数:将yield改成await,*号改成async,并内置执行器,返回promise对象。
Class:类。
继承使用extends关键字。在调用super()后才可以使用this关键字。
Decorator修饰器:
用来修改类的行为。@ core-decorator第三方库提供常见修饰器。(@autobind(this绑定原始对象) @readonly(属性方法不可写) @override(检查子类方法是否正确覆盖父类的同名方法。)等)
Module语法:模块化 import导入 export(default)导出。
CommonJS模块是运行时加载,ES6模块是编译时输出接口;
CommonJS模块输出的是值的拷贝,ES6模块输出的是值的引用。
以下是详细摘要及总结:
---------------------------------------------------------------------------------------------------------------------------------------------------------------
1.let const声明变量。
1.let:不存在变量提升,变量需要声明后使用,不允许重复声明,适用for循环等。
2.const:const保证的不是变量的值不得改动,而是变量指向的内存地址不得改动,(例如可以为对象添加属性,不能将对象指向别的对象。)适用于模块内声明变量等。
3.es6声明变量的六种方法 var function let const import class.
2.解构赋值。(一些栗子)
1 let [a, [b], d] = [1, [2, 3], 4]; 2 a // 1 3 b // 2 4 d // 4 5 6 let [x = 1] = [undefined]; 7 x // 1 8 9 let [x = 1] = [null]; 10 x // null 11 12 13 //用变量使用Math的方法 14 let { log, sin, cos } = Math; 15 16 17 //由于数组本质是特殊的对象,所以可以对数组进行对象属性的解构 18 let arr = [1, 2, 3]; 19 let {0 : first, [arr.length - 1] : last} = arr; 20 first // 1 21 last // 3 22 23 24 //函数参数的解构也可以使用默认值。 25 function move({x = 0, y = 0} = {}) { 26 return [x, y]; 27 } 28 29 move({x: 3, y: 8}); // [3, 8] 30 move({x: 3}); // [3, 0] 31 move({}); // [0, 0] 32 move(); // [0, 0 33 34 //以下是一些用途: 35 //交换数值 36 let x = 1; 37 let y = 2; 38 39 [x, y] = [y, x]; 40 41 42 // 返回一个数组 43 44 function example() { 45 return [1, 2, 3]; 46 } 47 let [a, b, c] = example(); 48 49 // 返回一个对象 50 51 function example() { 52 return { 53 foo: 1, 54 bar: 2 55 }; 56 } 57 let { foo, bar } = example(); 58 59 //将一组参数与变量名对应起来。 60 function f([x, y, z]) { ... } 61 f([1, 2, 3]); 62 63 // 参数是一组无次序的值 64 function f({x, y, z}) { ... } 65 f({z: 3, y: 2, x: 1}); 66 67 //提取json数据很有效 68 let jsonData = { 69 id: 42, 70 status: "OK", 71 data: [867, 5309] 72 }; 73 74 let { id, status, data: number } = jsonData; 75 76 console.log(id, status, number); 77 // 42, "OK", [867, 5309] 78 79 //输入模块的指定方法 80 const { SourceMapConsumer, SourceNode } = require("source-map");
3.字符串的扩展。
1.可以使用for...of遍历字符串 ;
2.新方法:includes(),startsWith(),endsWith(),repeat() padstart(),padEnd() (补全字符串);
3.模板字符串 ` ......${a}......` (非常实用);
4.函数的扩展。
1.函数可以使用默认参数,自动声明,不能用let或const再次声明,参数默认值是惰性求值(每次调用函数都会重新计算).
//若参数中没有={}则foo()会报错 function foo({x, y = 5}={}) { console.log(x, y); } foo({}) // undefined 5 foo({x: 1}) // 1 5 foo({x: 1, y: 2}) // 1 2 foo() // undefined 5
指定默认值后,函数的length属性返回没有指定默认值的参数的个数。
可以利用参数默认值,指定某个参数不能省略,如果省略就抛出错误,如下:
function throwIfMissing() { throw new Error(‘Missing parameter‘); } function foo(mustBeProvided = throwIfMissing()) { return mustBeProvided; } foo() // Error: Missing parameter
参数的默认值不是在定义时执行,而是在运行时执行,若将默认值设为undefined,表明这个参数可以省略。
2.用rest参数(...)取代arguments对象.
function add(...values) { let sum = 0; for (var val of values) { sum += val; } return sum; } add(2, 5, 3) // 10 // arguments变量的写法 function sortNumbers() { return Array.prototype.slice.call(arguments).sort(); } // rest参数的写法 const sortNumbers = (...numbers) => numbers.sort();
3.箭头函数.
var f = () => 5; // 等同于 var f = function () { return 5 }; var sum = (num1, num2) => num1 + num2; // 等同于 var sum = function(num1, num2) { return num1 + num2; }; var sum = (num1, num2) => { return num1 + num2; } // 报错 let getTempItem = id => { id: id, name: "Temp" }; // 不报错 let getTempItem = id => ({ id: id, name: "Temp" }); //只有一行且不需要返回值时可以省略大括号 let fn = () => void doesNotReturn(); //表达更简洁 const isEven = n => n % 2 == 0; const square = n => n * n; // 正常函数写法 var result = values.sort(function (a, b) { return a - b; }); // 箭头函数写法 var result = values.sort((a, b) => a - b); // 正常函数写法 [1,2,3].map(function (x) { return x * x; }); // 箭头函数写法 [1,2,3].map(x => x * x);
注意点:
函数体内的this对象是定义时所在的对象,而不是使用时所在的对象。(不能使用new),没有arguments对象(用rest...代替),
不能用作Generator(生成器)函数.
//this区别栗子 箭头函数的this绑定Timer function Timer() { this.s1 = 0; this.s2 = 0; // 箭头函数 setInterval(() => this.s1++, 1000); // 普通函数 setInterval(function () { this.s2++; }, 1000); } var timer = new Timer(); setTimeout(() => console.log(‘s1: ‘, timer.s1), 3100); setTimeout(() => console.log(‘s2: ‘, timer.s2), 3100); // s1: 3 // s2: 0 var handler = { id: ‘123456‘, init: function() { document.addEventListener(‘click‘, event => this.doSomething(event.type), false); }, doSomething: function(type) { console.log(‘Handling ‘ + type + ‘ for ‘ + this.id); } }; /*上面代码的init方法中,使用了箭头函数,这导致这个箭头函数里面的this,总是指向handler对象。否则,回调函数运行时,this.doSomething这一行会报错,因为此时this指向document对象。*/
深究:其实箭头函数没有自己的this.所以使用的是外层的this,所以不能用call apply bind方法.(可以用::替代)
4.尾递归优化:将递归改写为循环;
5.尾调用优化:只保留内层函数的调用帧(简写)
5.数组的扩展。
1.扩展运算符(...) 是rest参数的逆运算。将数组转为逗号分隔的参数序列。
即:三个点(...)在函数参数里使用时,代表一个数组;在函数调用的()里使用时,代表参数序列。
可以替代数组的apply方法,如下:
// ES5 的写法 function f(x, y, z) { // ... } var args = [0, 1, 2]; f.apply(null, args); // ES6的写法 function f(x, y, z) { // ... } let args = [0, 1, 2]; f(...args);
扩展运算符的应用:
a,复制数组 例如: let a2=[...a1] ;
b.合并数组 例如: [...arr1,...arr2,...arr3] 类似es5的concat方法;
c.与解构赋值结合 例如:[a,...rest]=list;
d.可以将字符串转为真正的数组 例如:[...‘hello‘] //["h","e","l","l","o"];
e.将实现了Iterator的类数组转为数组 例如[...nodelist] (例外:若类数组无Iterator接口可以用Array.from转化成数组,如下:)
let arrayLike = { ‘0‘: ‘a‘, ‘1‘: ‘b‘, ‘2‘: ‘c‘, length: 3 }; // TypeError: Cannot spread non-iterable object. let arr = [...arrayLike]; //可以使用Array.from(arrayLike)
f.可以用于Map、Set结构和Generator函数
2.Array.from() :将类数组对象和可遍历的对象转为数组 ,只要有length属性即可(如Array.from{length:3} //[undefined*3])
如下:(es5的替代方法为Array.prototype.slice)
// NodeList对象 let ps = document.querySelectorAll(‘p‘); Array.from(ps).forEach(function (p) { console.log(p); }); // arguments对象 function foo() { var args = Array.from(arguments); // ... }
Array.from还接受第二个参数,可以对每个元素进行处理,将处理后的值放入返回的数组。
Array.from(arrayLike, x => x * x); // 等同于 Array.from(arrayLike).map(x => x * x); Array.from([1, 2, 3], (x) => x * x) // [1, 4, 9]
扩展应用:
Array.from({ length: 2 }, () => ‘jack‘) // [‘jack‘, ‘jack‘]
以上代码的第一个参数指定了第二个参数运行的次数,很灵活。
3.Array.of():可以替代Array()方法,行为更统一。 (实现为return [].slice.call(arguments);
4.copyWithin() :将指定位置的成员复制到其他位置(会覆盖),然后返回当前数组。
5.数组实例的find()和findIndex()方法:
find():找出第一个符合条件的数组成员(第一个为true的成员),若没有,返回undefined;
[1, 5, 10, 15].find(function(value, index, arr) { return value > 9; }) // 10
findIndex():找出第一个符合条件的数组成员的位置,若没有,返回-1;
这两个方法都可以接受第二个参数,绑定回调函数的this对象。
6.fill()方法:填充数组,如下:
[‘a‘, ‘b‘, ‘c‘].fill(7, 1, 2) // [‘a‘, 7, ‘c‘] //三个参数分别是填充的值,开始位置和结束位置
7.数组实例的entries(),keys()和values():用于for...of循环遍历。
8.includes():类似字符串的includes方法,比indexOf()更语义化一些,也不会对NaN造成误判。
[1, 2, 3].includes(3, 3); // false [1, 2, 3].includes(3, -1); // true
9:数组的空位:es6将空位转为undefined , es5则比较混乱。
6.对象的扩展。
1.属性/方法名的简写:注意:简写的属性/方法名是字符串,不属于关键字。(可以用class()等)
2.属性名表达式:可以在对象中将表达式放在方括号里,注意:如果属性名表达式是一个对象,会将对象转为字符串[object object],需要注意。如下:
const keyA = {a: 1}; const keyB = {b: 2}; const myObject = { [keyA]: ‘valueA‘, [keyB]: ‘valueB‘ }; myObject // Object {[object Object]: "valueB"}
3.Object.is() 类似=== (不同之处 +0不等于-0,NaN等于自身)
4.Object.assign() 拷贝(属性相同会覆盖,只拷贝可以枚举的属性,不拷贝继承属性)
const v1 = ‘abc‘; const v2 = true; const v3 = 10; const obj = Object.assign({}, v1, v2, v3); console.log(obj); // { "0": "a", "1": "b", "2": "c" }
以上,除了字符串以数组形式拷贝入目标对象,其他无效果。
注意点:
a.Object.assign()是浅拷贝,如果源对象某个属性的值是对象,那么拷贝得到的是它的引用,互相影响;
b.同名属性的替换;
c.若用来处理数组,会把数组视为对象;
常见用途:
a.为对象加属性/方法,如下:
//添加属性
class Point { constructor(x, y) { Object.assign(this, {x, y}); } } //添加方法 Object.assign(SomeClass.prototype, { someMethod(arg1, arg2) { ··· }, anotherMethod() { ··· } }); // 等同于下面的写法 SomeClass.prototype.someMethod = function (arg1, arg2) { ··· }; SomeClass.prototype.anotherMethod = function () { ··· };
b.克隆对象,如下:
function clone(origin) { return Object.assign({}, origin); } //若想保持继承链,可以如下: function clone(origin) { let originProto = Object.getPrototypeOf(origin); return Object.assign(Object.create(originProto), origin); }
c.合并对象,如下:
const merge =
(...sources) => Object.assign({}, ...sources);
5.属性的可枚举性和遍历:可以使用Object.getOwnPropertyDescriptor(obj,‘name‘)获取属性的描述对象。
有四个操作会忽略enumerable为false的属性:for ..in Object.keys() JSON.stringify() Object.assign()。
其中只有for...in会返回继承的属性。可以用Object.keys()代替for...in;
6.Object.getOwnPropertyDescriptors返回指定对象所有自身属性(非继承属性)的描述对象;
7.__proto__:内部属性,可以用Object.setPrototypeOf() Object.getPrototypeOf() Object.create()代替。
let proto = {}; let obj = { x: 10 }; Object.setPrototypeOf(obj, proto); proto.y = 20; proto.z = 40; obj.x // 10 obj.y // 20 obj.z // 40
function Rectangle() { // ... } const rec = new Rectangle(); Object.getPrototypeOf(rec) === Rectangle.prototype // true Object.setPrototypeOf(rec, Object.prototype); Object.getPrototypeOf(rec) === Rectangle.prototype // false
8.super关键字(注意:目前,只有对象方法的简写法可以让 JavaScript 引擎确认,定义的是对象的方法。)
它指向当前对象的原型对象。
9.Object.keys() Object.values() Object.entries()
let {keys, values, entries} = Object; let obj = { a: 1, b: 2, c: 3 }; for (let key of keys(obj)) { console.log(key); // ‘a‘, ‘b‘, ‘c‘ } for (let value of values(obj)) { console.log(value); // 1, 2, 3 } for (let [key, value] of entries(obj)) { console.log([key, value]); // [‘a‘, 1], [‘b‘, 2], [‘c‘, 3] }
10.对象的扩展运算符
a.解构赋值:浅拷贝(若有负荷类型的值则拷贝引用,会互相影响,且不能复制继承自原型的属性(普通解构赋值可以继承到))
b.扩展运算符可以合并两个对象
let ab = { ...a, ...b }; // 等同于 let ab = Object.assign({}, a, b);
11.Null传导运算符:判断对象是否存在时的简写。 如:message?.body?.user?.firstname||‘default‘
7.新的原始数据类型Symbol
表示独一无二的值,可以用Object.getOwnPropertySymbols获取到,不会被普通方法遍历到,所以可以定义一些内部的变量等。
8.Set和Map结构
可以用[...new Set(array)]去除数组的重复成员(或者Array.from(new Set(array)))
Set结构的属性:.size .constructor 方法:add(value) delete(value) has(value) clear()
// 对象的写法 const properties = { ‘width‘: 1, ‘height‘: 1 }; if (properties[someName]) { // do something } // Set的写法 const properties = new Set(); properties.add(‘width‘); properties.add(‘height‘); if (properties.has(someName)) { // do something }
Set的遍历操作:keys() values() entries() forEach() 注意点:Set的遍历顺序就是插入顺序。
Map结构是一种值-值对的数据结构 属性/方法:size set(key,value) get(key) has(key) delete(key) clear()
WeakSet/WeakMap使用场景 :在DOM元素上添加数据时,可以不用手动删除引用(避免内存泄漏)。如下:
const e1 = document.getElementById(‘foo‘); const e2 = document.getElementById(‘bar‘); const arr = [ [e1, ‘foo 元素‘], [e2, ‘bar 元素‘], ]; // 不需要 e1 和 e2 的时候 // 必须手动删除引用 arr [0] = null; arr [1] = null; //若使用WeakMap()可以不手动释放对象。
9.Promise对象(用来传递异步操作的数据(消息))
缺点:
有了Promise
对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise
对象提供统一的接口,使得控制异步操作更加容易。
Promise
也有一些缺点。首先,无法取消Promise
,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise
内部抛出的错误,不会反应到外部。第三,当处于pending
状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
如果某些事件不断地反复发生,一般来说,使用Stream模式是比部署Promise
更好的选择。
基本用法:
a.Promise 新建后立即执行,然后,then
方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行。
下面是一个用Promise
对象实现的 Ajax 操作的例子。
const getJSON = function(url) { const promise = new Promise(function(resolve, reject){ const handler = function() { if (this.readyState !== 4) { return; } if (this.status === 200) { resolve(this.response); } else { reject(new Error(this.statusText)); } }; const client = new XMLHttpRequest(); client.open("GET", url); client.onreadystatechange = handler; client.responseType = "json"; client.setRequestHeader("Accept", "application/json"); client.send(); }); return promise; }; getJSON("/posts.json").then(function(json) { console.log(‘Contents: ‘ + json); }, function(error) { console.error(‘出错了‘, error); });
b.resolve函数的参数可以是另一个Promise实力,如下:
const p1 = new Promise(function (resolve, reject) { setTimeout(() => reject(new Error(‘fail‘)), 3000) }) const p2 = new Promise(function (resolve, reject) { setTimeout(() => resolve(p1), 1000) }) p2 .then(result => console.log(result)) .catch(error => console.log(error)) // Error: fail
上面代码中,p1
是一个 Promise,3 秒之后变为rejected
。p2
的状态在 1 秒之后改变,resolve
方法返回的是p1
。由于p2
返回的是另一个 Promise,导致p2
自己的状态无效了,由p1
的状态决定p2
的状态。所以,后面的then
语句都变成针对p1。又过了 2 秒,p1
变为rejected
,导致触发catch
方法指定的回调函数。
c.良好习惯:在resolve和reject前加上return,后续操作放在then方法里。
Promise.prototype.then():
getJSON("/post/1.json").then( post => getJSON(post.commentURL)//相当于es5的return getJSON(...) ).then( comments => console.log("resolved: ", comments),//resolve时 err => console.log("rejected: ", err)//reject时 );
Promise.prototype.catch():
Promise.prototype.catch
方法是.then(null, rejection)
的别名,用于指定发生错误时的回调函数。
一般来说,不要在then
方法里面定义 Reject 状态的回调函数(即then
的第二个参数),总是使用catch
方法。
一般总是建议,Promise 对象后面要跟catch
方法,这样可以处理 Promise 内部发生的错误。catch
方法返回的还是一个 Promise 对象,因此后面还可以接着调用then
方法。Promise 在resolve
语句后面,再抛出错误,不会被捕获,等于没有抛出。因为 Promise 的状态一旦改变,就永久保持该状态,不会再变了。
Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch
语句捕获。
const someAsyncThing = function() { return new Promise(function(resolve, reject) { // 下面一行会报错,因为x没有声明 resolve(x + 2); }); }; someAsyncThing().then(function() { return someOtherAsyncThing(); }).catch(function(error) { console.log(‘oh no‘, error); // 下面一行会报错,因为 y 没有声明 y + 2; }).then(function() { console.log(‘carry on‘); }); // oh no [ReferenceError: x is not defined]
上面代码中,catch
方法抛出一个错误,因为后面没有别的catch
方法了,导致这个错误不会被捕获,也不会传递到外层.
可以改写如下:
someAsyncThing().then(function() { return someOtherAsyncThing(); }).catch(function(error) { console.log(‘oh no‘, error); // 下面一行会报错,因为y没有声明 y + 2; }).catch(function(error) { console.log(‘carry on‘, error); }); // oh no [ReferenceError: x is not defined] // carry on [ReferenceError: y is not defined]
Promise.all():
Promise.all
方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。(const p = Promise.all([p1, p2, p3]);
p
的状态由p1
、p2
、p3
决定,分成两种情况。
(1)只有p1
、p2
、p3
的状态都变成fulfilled
,p
的状态才会变成fulfilled
,此时p1
、p2
、p3
的返回值组成一个数组,传递给p
的回调函数。
(2)只要p1
、p2
、p3
之中有一个被rejected
,p
的状态就变成rejected
,此时第一个被reject
的实例的返回值,会传递给p
的回调函数。例子如下:
const databasePromise = connectDatabase(); const booksPromise = databasePromise .then(findAllBooks); const userPromise = databasePromise .then(getCurrentUser); Promise.all([ booksPromise, userPromise ]) .then(([books, user]) => pickTopRecommentations(books, user));
注意,如果作为参数的 Promise 实例,自己定义了catch
方法,那么它一旦被rejected
,并不会触发Promise.all()
的catch
方法。如果p2
没有自己的catch
方法,就会调用Promise.all()
的catch
方法。
Promise.race():
const p = Promise.race([p1, p2, p3]);
只要p1
、p2
、p3
之中有一个实例率先改变状态,p
的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p
的回调函数。
例子:
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(response => console.log(response)); p.catch(error => console.log(error));
如果 5 秒之内fetch
方法无法返回结果,变量p
的状态就会变为rejected
,从而触发catch
方法指定的回调函数。
Promise.resolve():将现有对象转为 Promise 对象.
当参数是一个thenable对象时,如下:
let thenable = { then: function(resolve, reject) { resolve(42); } }; let p1 = Promise.resolve(thenable); p1.then(function(value) { console.log(value); // 42 });
Promise.resolve
方法会将这个对象转为 Promise 对象,然后就立即执行thenable
对象的then
方法。
注意点:立即resolve
的 Promise 对象,是在本轮“事件循环”(event loop)的结束时,而不是在下一轮“事件循环”的开始时。
setTimeout(function () { console.log(‘three‘); }, 0); Promise.resolve().then(function () { console.log(‘two‘); }); console.log(‘one‘); // one // two // three
Promise.reject():
const p = Promise.reject(‘出错了‘); // 等同于 const p = new Promise((resolve, reject) => reject(‘出错了‘)) p.then(null, function (s) { console.log(s) }); // 出错了
Promise.reject(reason)
方法也会返回一个新的 Promise 实例,该实例的状态为rejected
。
注意,Promise.reject()
方法的参数,会原封不动地作为reject
的理由,变成后续方法的参数。这一点与Promise.resolve
方法不一致。
可以自己部署的方法:
done():用于代码最后捕捉错误。
实现如下:
Promise.prototype.done = function (onFulfilled, onRejected) { this.then(onFulfilled, onRejected) .catch(function (reason) { // 抛出一个全局错误 setTimeout(() => { throw reason }, 0); }); };
finally():finally
方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。它与done
方法的最大区别,它接受一个普通的回调函数作为参数,该函数不管怎样都必须执行。
server.listen(0) .then(function () { // run test }) .finally(server.stop); //最后用finally关掉服务器,无论结果如何
实现如下:
Promise.prototype.finally = function (callback) { let P = this.constructor; return this.then( value => P.resolve(callback()).then(() => value), reason => P.resolve(callback()).then(() => { throw reason }) ); };
Promise.try():让同步函数同步执行,异步函数异步执行,并且让它们具有统一的 API
第一种写法:
(async () => f())() .then(...) .catch(...)
第二种写法:
const f = () => console.log(‘now‘); ( () => new Promise( resolve => resolve(f()) ) )(); console.log(‘next‘); // now // next
用Promise.try(),如下:
const f = () => console.log(‘now‘); Promise.try(f); console.log(‘next‘); // now // next
由于Promise.try
为所有操作提供了统一的处理机制,所以如果想用then
方法管理流程,最好都用Promise.try
包装一下。这样有很多好处,其中一点就是可以更好地管理异常。
10.Iterator(遍历器)和for...of
1.a.任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。主要供for...of消费。
ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator
属性,或者说,一个数据结构只要具有Symbol.iterator
属性,就可以认为是“可遍历的”(iterable)。
例如:
const obj = { [Symbol.iterator] : function () { return { next: function () { return { value: 1, done: true }; } }; } };
b.原生具备 Iterator 接口的数据结构如下。
- Array
- Map
- Set
- String
- TypedArray
- 函数的 arguments 对象
- NodeList 对象
一个对象如果要具备可被for...of
循环调用的 Iterator 接口,就必须在Symbol.iterator
的属性上部署(也可以修改)遍历器生成方法。(或者使用Map结构)
let it=something[Symbol.iterator]() it.next()...
c.遍历器对象也可以部署return()和throw()方法,如下:
function readLinesSync(file) { return { next() { return { done: false }; }, return() { file.close(); return { done: true }; }, }; } // 情况一 for (let line of readLinesSync(fileName)) { console.log(line); break; } // 情况二 for (let line of readLinesSync(fileName)) { console.log(line); continue; } // 情况三 for (let line of readLinesSync(fileName)) { console.log(line); throw new Error(); } //3种情况都会触发return
上面代码中,情况一输出文件的第一行以后,就会执行return
方法,关闭这个文件;情况二输出所有行以后,执行return
方法,关闭该文件;情况三会在执行return
方法关闭文件之后,再抛出错误。注意,return
方法必须返回一个对象,这是 Generator 规格决定的.
2.for...of(内部调用Symbol.iterator方法)
for...of
循环可以使用的范围包括数组、Set 和 Map 结构、某些类似数组的对象(比如arguments
对象、DOM NodeList 对象)、 Generator 对象,以及字符串。
for...in
循环读取键名,for...of
循环读取键值。如果要通过for...of
循环,获取数组的索引,可以借助数组实例的entries
方法和keys
方法
对象:对于普通的对象,for...of
结构不能直接使用,会报错,必须部署了 Iterator 接口后才能使用。但是,这样情况下,for...in
循环依然可以用来遍历键名。
解决方案:
for (var key of Object.keys(someObject)) { console.log(key + ‘: ‘ + someObject[key]); }
与其他遍历语法的比较:
for循环比较麻烦。forEach方法无法中途跳出。break return都无效。for...in的缺点:
- 数组的键名是数字,但是
for...in
循环是以字符串作为键名“0”、“1”、“2”等等。 for...in
循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。- 某些情况下,
for...in
循环会以任意顺序遍历键名。 for...in
循环主要是为遍历对象而设计的,不适用于遍历数组。
for...of可以与break continue return配合使用。
11.Generator函数
1.a.Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象(遍历器对象)
每次调用next
方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield
表达式(或return
语句)为止。换言之,Generator 函数是分段执行的,yield
表达式是暂停执行的标记,而next
方法可以恢复执行。
function* helloWorldGenerator() { yield ‘hello‘; yield ‘world‘; return ‘ending‘; } var hw = helloWorldGenerator(); hw.next() // { value: ‘hello‘, done: false } hw.next() // { value: ‘world‘, done: false } hw.next() // { value: ‘ending‘, done: true } hw.next() // { value: undefined, done: true }
b.yield表达式
遍历器对象的next
方法的运行逻辑如下。
(1)遇到yield
表达式,就暂停执行后面的操作,并将紧跟在yield
后面的那个表达式的值,作为返回的对象的value
属性值。
(2)下一次调用next
方法时,再继续往下执行,直到遇到下一个yield
表达式。
(3)如果没有再遇到新的yield
表达式,就一直运行到函数结束,直到return
语句为止,并将return
语句后面的表达式的值,作为返回的对象的value
属性值。
(4)如果该函数没有return
语句,则返回的对象的value
属性值为undefined
。
yield
表达式如果用在另一个表达式之中,必须放在圆括号里面。
c.与Iterator接口的关系:
可以把 Generator 赋值给对象的Symbol.iterator
属性,从而使得该对象具有 Iterator 接口。
var myIterable = {}; myIterable[Symbol.iterator] = function* () { yield 1; yield 2; yield 3; }; [...myIterable] // [1, 2, 3]
2、next方法的参数
yield
表达式本身没有返回值,或者说总是返回undefined
。next
方法可以带一个参数,该参数就会被当作上一个yield
表达式的返回值。
function* f() { for(var i = 0; true; i++) { var reset = yield i; if(reset) { i = -1; } } } var g = f(); g.next() // { value: 0, done: false } g.next() // { value: 1, done: false } g.next(true) // { value: 0, done: false }
function* foo(x) { var y = 2 * (yield (x + 1)); var z = yield (y / 3); return (x + y + z); } var a = foo(5); a.next() // Object{value:6, done:false} a.next() // Object{value:NaN, done:false} a.next() // Object{value:NaN, done:true} var b = foo(5); b.next() // { value:6, done:false } b.next(12) // { value:8, done:false } b.next(13) // { value:42, done:true }
3.for...of循环
for...of
循环可以自动遍历 Generator 函数时生成的Iterator
对象,且此时不再需要调用next
方法。(不包括return的值)
原生的 JavaScript 对象没有遍历接口,无法使用for...of
循环,通过 Generator 函数为它加上这个接口,就可以用了。如下:
function* objectEntries() { let propKeys = Object.keys(this); for (let propKey of propKeys) { yield [propKey, this[propKey]]; } } let jane = { first: ‘Jane‘, last: ‘Doe‘ }; jane[Symbol.iterator] = objectEntries; for (let [key, value] of jane) { console.log(`${key}: ${value}`); } // first: Jane // last: Doe
除了for...of
循环以外,扩展运算符(...
)、解构赋值和Array.from
方法内部调用的,都是遍历器接口。这意味着,它们都可以将 Generator 函数返回的 Iterator 对象,作为参数。
function* numbers () { yield 1 yield 2 return 3 yield 4 } // 扩展运算符 [...numbers()] // [1, 2] // Array.from 方法 Array.from(numbers()) // [1, 2] // 解构赋值 let [x, y] = numbers(); x // 1 y // 2 // for...of 循环 for (let n of numbers()) { console.log(n) } // 1 // 2
4.Generator.prototype.throw():
Generator 函数返回的遍历器对象,都有一个throw
方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。
throw
方法可以接受一个参数,该参数会被catch
语句接收,建议抛出Error
对象的实例。
不要混淆遍历器对象的throw
方法和全局的throw
命令。上面代码的错误,是用遍历器对象的throw
方法抛出的,而不是用throw
命令抛出的。后者只能被函数体外的catch
语句捕获。
var g = function* () { while (true) { try { yield; } catch (e) { if (e != ‘a‘) throw e; console.log(‘内部捕获‘, e); } } }; var i = g(); i.next(); try { throw new Error(‘a‘); throw new Error(‘b‘); } catch (e) { console.log(‘外部捕获‘, e); } // 外部捕获 [Error: a]
throw
方法被捕获以后,会附带执行下一条yield
表达式。也就是说,会附带执行一次next
方法。
var gen = function* gen(){ try { yield console.log(‘a‘); } catch (e) { // ... } yield console.log(‘b‘); yield console.log(‘c‘); } var g = gen(); g.next() // a g.throw() // b g.next() // c
上面代码中,g.throw
方法被捕获以后,自动执行了一次next
方法,所以会打印b
。另外,也可以看到,只要 Generator 函数内部部署了try...catch
代码块,那么遍历器的throw
方法抛出的错误,不影响下一次遍历。
这种函数体内捕获错误的机制,大大方便了对错误的处理。多个yield
表达式,可以只用一个try...catch
代码块来捕获错误。如果使用回调函数的写法,想要捕获多个错误,就不得不为每个函数内部写一个错误处理语句,现在只在 Generator 函数内部写一次catch
语句就可以了。
5.Generator.prototype.return():
Generator 函数返回的遍历器对象,还有一个return
方法,可以返回给定的值,并且终结遍历 Generator 函数。
如果 Generator 函数内部有try...finally
代码块,那么return
方法会推迟到finally
代码块执行完再执行。
6.yield*表达式:
yield*
表达式,用来在一个 Generator 函数里面执行另一个 Generator 函数。
yield*
后面的 Generator 函数(没有return
语句时),等同于在 Generator 函数内部,部署一个for...of
循环。
function* concat(iter1, iter2) { yield* iter1; yield* iter2; } // 等同于 function* concat(iter1, iter2) { for (var value of iter1) { yield value; } for (var value of iter2) { yield value; } }
上面代码说明,yield*
后面的 Generator 函数(没有return
语句时),不过是for...of
的一种简写形式,完全可以用后者替代前者。反之,在有return
语句时,则需要用var value = yield* iterator
的形式获取return
语句的值。
任何数据结构只要有 Iterator 接口,就可以被yield*
遍历。
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
7.作为对象属性的Generator函数:
let obj = { * myGeneratorMethod() { ··· } }; //等同于 let obj = { myGeneratorMethod: function* () { // ··· } };
8. Generator函数的this
Generator 函数总是返回一个遍历器,ES6 规定这个遍历器是 Generator 函数的实例,也继承了 Generator 函数的prototype
对象上的方法。
function* g() {} g.prototype.hello = function () { return ‘hi!‘; }; let obj = g(); obj instanceof g // true obj.hello() // ‘hi!‘
如果把g
当作普通的构造函数,并不会生效,因为g
返回的总是遍历器对象,而不是this
对象。也不能跟new
命令一起用,会报错。于是可以
将Generator 函数改造成构造函数:
function* gen() { this.a = 1; yield this.b = 2; yield this.c = 3; } function F() { return gen.call(gen.prototype); } var f = new F(); 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
9.含义:
a.Generator 是实现状态机的最佳结构
var ticking = true; var clock = function() { if (ticking) console.log(‘Tick!‘); else console.log(‘Tock!‘); ticking = !ticking; }//es5 var clock = function* () { while (true) { console.log(‘Tick!‘); yield; console.log(‘Tock!‘); yield; } };//es6
上面的 Generator 实现与 ES5 实现对比,可以看到少了用来保存状态的外部变量ticking
,这样就更简洁,更安全(状态不会被非法篡改)、更符合函数式编程的思想,在写法上也更优雅。Generator 之所以可以不用外部变量保存状态,是因为它本身就包含了一个状态信息,即目前是否处于暂停态。
b.Generator与协程:
多个线程(单线程情况下,即多个函数)可以并行执行,但是只有一个线程(或函数)处于正在运行的状态,其他线程(或函数)都处于暂停态(suspended),线程(或函数)之间可以交换执行权。也就是说,一个线程(或函数)执行到一半,可以暂停执行,将执行权交给另一个线程(或函数),等到稍后收回执行权的时候,再恢复执行。这种可以并行执行、交换执行权的线程(或函数),就称为协程。
从实现上看,在内存中,子例程只使用一个栈(stack),而协程是同时存在多个栈,但只有一个栈是在运行状态,也就是说,协程是以多占用内存为代价,实现多任务的并行。
由于 JavaScript 是单线程语言,只能保持一个调用栈。引入协程以后,每个任务可以保持自己的调用栈。这样做的最大好处,就是抛出错误的时候,可以找到原始的调用栈。不至于像异步操作的回调函数那样,一旦出错,原始的调用栈早就结束。
Generator 与上下文
JavaScript 代码运行时,会产生一个全局的上下文环境(context,又称运行环境),包含了当前所有的变量和对象。然后,执行函数(或块级代码)的时候,又会在当前上下文环境的上层,产生一个函数运行的上下文,变成当前(active)的上下文,由此形成一个上下文环境的堆栈(context stack)。
这个堆栈是“后进先出”的数据结构,最后产生的上下文环境首先执行完成,退出堆栈,然后再执行完成它下层的上下文,直至所有代码执行完成,堆栈清空。
Generator 函数不是这样,它执行产生的上下文环境,一旦遇到yield
命令,就会暂时退出堆栈,但是并不消失,里面的所有变量和对象会冻结在当前状态。等到对它执行next
命令时,这个上下文环境又会重新加入调用栈,冻结的变量和对象恢复执行。
function *gen() { yield 1; return 2; } let g = gen(); console.log( g.next().value, g.next().value, );
上面代码中,第一次执行g.next()
时,Generator 函数gen
的上下文会加入堆栈,即开始运行gen
内部的代码。等遇到yield 1
时,gen
上下文退出堆栈,内部状态冻结。第二次执行g.next()
时,gen
上下文重新加入堆栈,变成当前的上下文,重新恢复执行。
10.应用:
a异步操作的同步化表达:
function* loadUI() { showLoadingScreen(); yield loadUIDataAsynchronously(); hideLoadingScreen(); } var loader = loadUI(); // 加载UI loader.next() // 卸载UI loader.next()
上面代码中,第一次调用loadUI
函数时,该函数不会执行,仅返回一个遍历器。下一次对该遍历器调用next
方法,则会显示Loading
界面(showLoadingScreen
),并且异步加载数据(loadUIDataAsynchronously
)。等到数据加载完成,再一次使用next
方法,则会隐藏Loading
界面。可以看到,这种写法的好处是所有Loading
界面的逻辑,都被封装在一个函数,按部就班非常清晰。
b.控制流管理:
function* longRunningTask(value1) { try { var value2 = yield step1(value1); var value3 = yield step2(value2); var value4 = yield step3(value3); var value5 = yield step4(value4); // Do something with value4 } catch (e) { // Handle any error from step1 through step4 } }
然后,使用一个函数,按次序自动执行所有步骤。
scheduler(longRunningTask(initialValue)); function scheduler(task) { var taskObj = task.next(task.value); // 如果Generator函数未结束,就继续调用 if (!taskObj.done) { task.value = taskObj.value scheduler(task); } }
注意,上面这种做法,只适合同步操作,即所有的task
都必须是同步的,不能有异步操作。因为这里的代码一得到返回值,就继续往下执行,没有判断异步操作何时完成。
利用for...of
循环会自动依次执行yield
命令的特性,提供一种更一般的控制流管理的方法。
let steps = [step1Func, step2Func, step3Func]; function *iterateSteps(steps){ for (var i=0; i< steps.length; i++){ var step = steps[i]; yield step(); } }
将任务分解成步骤之后,还可以将项目分解成多个依次执行的任务。
let jobs = [job1, job2, job3]; function* iterateJobs(jobs){ for (var i=0; i< jobs.length; i++){ var job = jobs[i]; yield* iterateSteps(job.steps); } }
上面代码中,数组jobs
封装了一个项目的多个任务,Generator 函数iterateJobs
则是依次为这些任务加上yield*
命令。
最后,就可以用for...of
循环一次性依次执行所有任务的所有步骤。
for (var step of iterateJobs(jobs)){ console.log(step.id); }
c.部署Iterator接口:
利用 Generator 函数,可以在任意对象上部署 Iterator 接口。
d.作为数据结构:
它可以对任意表达式,提供类似数组的接口。(可以用for...of遍历)
11.Generator函数的异步应用:
Generator 函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因。除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案:函数体内外的数据交换和错误处理机制。
Thunk函数:
编译器的“传名调用”实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数。
function f(m) { return m * 2; } f(x + 5); // 等同于 var thunk = function () { return x + 5; }; function f(thunk) { return thunk() * 2; }
JavaScript 语言是传值调用,它的 Thunk 函数含义有所不同。在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数。
Thunkify模块:生产环境的转换器,建议使用 Thunkify 模块。
Thunk 函数现在可以用于 Generator 函数的自动流程管理。(可以自动执行 Generator 函数)
co模块:
co 模块其实就是将两种自动执行器(Thunk 函数和 Promise 对象),包装成一个模块。使用 co 的前提条件是,Generator 函数的yield
命令后面,只能是 Thunk 函数或 Promise 对象。如果数组或对象的成员,全部都是 Promise 对象,也可以使用 co。
co 支持并发的异步操作,即允许某些操作同时进行,等到它们全部完成,才进行下一步。
这时,要把并发的操作都放在数组或对象里面,跟在yield
语句后面。
// 数组的写法 co(function* () { var res = yield [ Promise.resolve(1), Promise.resolve(2) ]; console.log(res); }).catch(onerror); // 对象的写法 co(function* () { var res = yield { 1: Promise.resolve(1), 2: Promise.resolve(2), }; console.log(res); }).catch(onerror);
co(function* () { var values = [n1, n2, n3]; yield values.map(somethingAsync); }); function* somethingAsync(x) { // do something async return y }
上面的代码允许并发三个somethingAsync
异步操作,等到它们全部完成,才会进行下一步。
12.Async函数
//一个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()); };
写成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 函数的改进,体现在以下四点。
(1)内置执行器。
async
函数的执行,与普通函数一模一样,只要一行。(例如:asyncReadFile();
)
它就会自动执行,输出最后结果。这完全不像 Generator 函数,需要调用next
方法,或者用co
模块,才能真正执行,得到最后结果。
(2)更好的语义。
(3)更广的适用性。
co
模块约定,yield
命令后面只能是 Thunk 函数或 Promise 对象,而async
函数的await
命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。
(4)返回值是 Promise。
async
函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then
方法指定下一步的操作。
进一步说,async
函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await
命令就是内部then
命令的语法糖。
基本用法:
async function getStockPriceByName(name) { const symbol = await getStockSymbol(name); const stockPrice = await getStockPrice(symbol); return stockPrice; } getStockPriceByName(‘goog‘).then(function (result) { console.log(result); });
上面代码是一个获取股票报价的函数,函数前面的async
关键字,表明该函数内部有异步操作。调用该函数时,会立即返回一个Promise
对象。
//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 () => {};
async
函数返回一个 Promise 对象。
async
函数内部return
语句返回的值,会成为then
方法回调函数的参数。
async function f() { return ‘hello world‘; } f().then(v => console.log(v)) // "hello world"
async
函数内部抛出错误,会导致返回的 Promise 对象变为reject
状态。抛出的错误对象会被catch
方法回调函数接收到。
async function f() { throw new Error(‘出错了‘); } f().then( v => console.log(v), e => console.log(e) ) // Error: 出错了
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 对象。如果不是,会被转成一个立即resolve
的 Promise 对象。
await
命令后面的 Promise 对象如果变为reject
状态,则reject
的参数会被catch
方法的回调函数接收到。
只要一个await
语句后面的 Promise 变为reject
,那么整个async
函数都会中断执行。
有时,我们希望即使前一个异步操作失败,也不要中断后面的异步操作。这时可以将第一个await
放在try...catch
结构里面,这样不管这个异步操作是否成功,第二个await
都会执行。
async function f() { try { await Promise.reject(‘出错了‘); } catch(e) { } return await Promise.resolve(‘hello world‘); } f() .then(v => console.log(v)) // hello world
另一种方法是await
后面的 Promise 对象再跟一个catch
方法,处理前面可能出现的错误。
async function f() { await Promise.reject(‘出错了‘) .catch(e => console.log(e)); return await Promise.resolve(‘hello world‘); } f() .then(v => console.log(v)) // 出错了 // hello world
如果await
后面的异步操作出错,那么等同于async
函数返回的 Promise 对象被reject
。防止出错的方法,也是将其放在try...catch
代码块之中。
如果有多个await
命令,可以统一放在try...catch
结构中。
下面的例子使用try...catch
结构,实现多次重复尝试。
const superagent = require(‘superagent‘); const NUM_RETRIES = 3; async function test() { let i; for (i = 0; i < NUM_RETRIES; ++i) { try { await superagent.get(‘http://google.com/this-throws-an-error‘); break; } catch(err) {} } console.log(i); // 3 } test();
使用注意点:
1.await
命令后面的Promise
对象,运行结果可能是rejected
,所以最好把await
命令放在try...catch
代码块中。
2.将继发写成同时触发:
//继发 let foo = await getFoo(); let bar = await getBar(); /同时触发 // 写法一 let [foo, bar] = await Promise.all([getFoo(), getBar()]); // 写法二 let fooPromise = getFoo(); let barPromise = getBar(); let foo = await fooPromise; let bar = await barPromise;
3.await
命令只能用在async
函数之中,如果用在普通函数,就会报错。
如果确实希望多个请求并发执行,可以使用Promise.all
方法。当三个请求都会resolved
时,下面两种写法效果相同。
async function dbFuc(db) { let docs = [{}, {}, {}]; let promises = docs.map((doc) => db.post(doc)); let results = await Promise.all(promises); console.log(results); } // 或者使用下面的写法 async function dbFuc(db) { let docs = [{}, {}, {}]; let promises = docs.map((doc) => db.post(doc)); let results = []; for (let promise of promises) { results.push(await promise); } console.log(results); }
一个栗子:
假定某个 DOM 元素上面,部署了一系列的动画,前一个动画结束,才能开始后一个。如果当中有一个动画出错,就不再往下执行,返回上一个成功执行的动画的返回值。
async function chainAnimationsAsync(elem, animations) { let ret = null; try { for(let anim of animations) { ret = await anim(elem); } } catch(e) { /* 忽略错误,继续执行 */ } return ret; }
另一个栗子:
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); } }
虽然map
方法的参数是async
函数,但它是并发执行的,因为只有async
函数内部是继发执行,外部不受影响。后面的for..of
循环内部使用了await
,因此实现了按顺序输出。
异步遍历器:
对象的异步遍历器接口,部署在Symbol.asyncIterator
属性上面。不管是什么样的对象,只要它的Symbol.asyncIterator
属性有值,就表示应该对它进行异步遍历。
for...of
循环用于遍历同步的 Iterator 接口。新引入的for await...of
循环,则是用于遍历异步的 Iterator 接口。
异步遍历器的设计目的之一,就是 Generator 函数处理同步操作和异步操作时,能够使用同一套接口。
13.Class
构造函数的prototype
属性,在 ES6 的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype
属性上面。
class Point { constructor() { // ... } toString() { // ... } toValue() { // ... } } // 等同于 Point.prototype = { constructor() {}, toString() {}, toValue() {}, };
类的内部所有定义的方法,都是不可枚举的(non-enumerable)。这一点与 ES5 的行为不一致。
Object.assign
方法可以很方便地一次向类添加多个方法。
class Point { constructor(){ // ... } } Object.assign(Point.prototype, { toString(){}, toValue(){} });
与函数一样,类也可以使用表达式的形式定义。
const MyClass = class Me { getClassName() { return Me.name; } };
let inst = new MyClass();
inst.getClassName() // Me
Me.name // ReferenceError: Me is not defined
上面代码使用表达式定义了一个类。需要注意的是,这个类的名字是MyClass
而不是Me
,Me
只在 Class 的内部代码可用,指代当前类。
如果类的内部没用到的话,可以省略Me
,也就是可以写成下面的形式。
采用 Class 表达式,可以写出立即执行的 Class。
let person = new class { constructor(name) { this.name = name; } sayName() { console.log(this.name); } }(‘张三‘); person.sayName(); // "张三"
类不存在变量提升(hoist),这一点与 ES5 完全不同。
私有方法:一种做法是在命名上加以区别。(_methods)
另一种方法就是索性将私有方法移出模块,因为模块内部的所有方法都是对外可见的。
class Widget { foo (baz) { bar.call(this, baz); } // baz变成当前函数的私有方法 } function bar(baz) { return this.snaf = baz; }
私有属性:(#x)
this的指向问题:
class Logger { constructor() { this.printName = this.printName.bind(this); } // ... }
class Logger { constructor() { this.printName = (name = ‘there‘) => { this.print(`Hello ${name}`); }; } // ... }
class的静态方法:
类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static
关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。
class Foo { static classMethod() { return ‘hello‘; } } Foo.classMethod() // ‘hello‘ var foo = new Foo(); foo.classMethod() // TypeError: foo.classMethod is not a function
父类的静态方法,可以被子类继承。静态方法也是可以从super
对象上调用的。
class的静态属性和实例属性:
// 老写法
class Foo { } Foo.prop = 1; Foo.prop // 1
// 新写法
class Foo {
static prop = 1;
}
ES6 为new
命令引入了一个new.target
属性,该属性一般用在构造函数之中,返回new
命令作用于的那个构造函数。如果构造函数不是通过new
命令调用的,new.target
会返回undefined。
class的继承:
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
方法和toString
方法之中,都出现了super
关键字,它在这里表示父类的构造函数,用来新建父类的this
对象。
子类必须在constructor
方法中调用super
方法,否则新建实例时会报错。这是因为子类没有自己的this
对象,而是继承父类的this
对象,然后对其进行加工。如果不调用super
方法,子类就得不到this
对象。
ES5 的继承,实质是先创造子类的实例对象this
,然后再将父类的方法添加到this
上面(Parent.apply(this)
)。ES6 的继承机制完全不同,实质是先创造父类的实例对象this
(所以必须先调用super
方法),然后再用子类的构造函数修改this
。
Object.getPrototypeOf
方法可以用来从子类上获取父类。Object.getPrototypeOf(ColorPoint) === Point,可以使用这个方法判断,一个类是否继承了另一个类。
super
这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。
大多数浏览器的 ES5 实现之中,每一个对象都有__proto__
属性,指向对应的构造函数的prototype
属性。Class 作为构造函数的语法糖,同时有prototype
属性和__proto__
属性,因此同时存在两条继承链。
(1)子类的__proto__
属性,表示构造函数的继承,总是指向父类。
(2)子类prototype
属性的__proto__
属性,表示方法的继承,总是指向父类的prototype
属性。
Mixin 指的是多个对象合成一个新的对象,新对象具有各个组成成员的接口。它的最简单实现如下。
14.Moduel
ES6 模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。(运行时加载)
ES6 模块不是对象,而是通过export
命令显式指定输出的代码,再通过import
命令输入。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。