序:
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 模块是编译时输出接口。