前言
阮一峰的《ECMAScript 6入门》太过详细了,全部看完需要花很长时间,所以,本文只是前者的一个超级精简版。
ES6简介
ECMAScript6.0
,简称ES6
,又叫ES2015
,是JavaScript语言的下一代标准,对JavaScript语法进行了比较大的修改。
对于不支持ES6的浏览器可以将ES6代码用转换工具转换成ES5语法。
ECMAScript
是JavaScript
的标准,JavaScript
是ECMAScript
的一种实现。
let和const
let
let
和var
类似,也是用来声明一个变量,不同的是它支持块级作用域。
let相对于var的区别:
- let支持
块级作用域
; - let不会发生
变量提升
; - 不允许重复声明(即使之前是用var声明的也不行);
- let声明的全局变量不属于window的子属性(也就是
let a = 1;
之后再调用window.a
依旧是undefined
); - 使用let或const声明变量之前该变量都是不可用的,否则报错。
补充:
如果块级作用域中存在
let
和const
命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。总之,在同一代码块内,使用let声明变量之前该变量都是不可用的,语法上称为暂时性死区
(temporal dead zone
,简称TDZ
);
const
const
用来声明一个只读的常量,一旦声明,常量的值就不能改变,所以const
一旦声明变量,就必须立即初始化,否则报错。const
和let
非常类似,具备上面提到的let具备的所有特点,比如块级作用域、不存在变量提升、暂时性死区、不能重复声明。
但是,const命令只是保证变量名指向的地址不变,并不保证该地址的数据不变,所以下面的代码是没问题的:
const obj = {aa: 1};
obj.bb = 2;
console.log(obj.bb); // 输出 2
所以实际使用中应当尽量避免这种情况,非要将对象声明成常量可以这样:
const obj = Object.freeze({aa: 1});
// 常规模式时,下面一行不起作用;
// 严格模式时,该行会报错
obj.bb = 2;
ES6声明变量的6种方法
ES5只有两种声明变量的方法:var
命令和function
命令。ES6除了添加let
和const
命令之外,还有2个:import
命令和class
命令。所以,ES6一共有6种声明变量的方法。
关于顶级对象
顶级对象,在浏览器环境指的是window
对象,在Node指的是global
对象。ES5之中,顶层对象的属性与全局变量是等价的。也就是用var声明的全局变量就是window的一个属性(有一点不同,就是var声明的变量无法使用delete删除,window声明的变量可以删除)。
顶层对象的属性与全局变量挂钩,被认为是JavaScript语言最大的设计败笔之一。
ES6为了改变这一点,一方面规定,为了保持兼容性,var
命令和function
命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let
命令、const
命令、class
命令声明的全局变量,不属于顶层对象的属性。
var a = 1;
console.log(window.a); // 1
let b = 1;
console.log(window.b); // undefined
变量的解构赋值
ES6
允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构Destructuring
。
各种类型的解构赋值示例
var [a, b, c] = [1, 2, 3]; // 相当于分别给a、b、c赋值
console.log(a, b, c); // 1, 2, 3
var [x, , y] = [1, 2, 3];
console.log(x, y); // 1,3
var [x, y = 'b'] = ['a']; // 赋默认值,结果:x='a', y='b'
console.log(x, y); // a, b
var [x, y, z] = new Set(["a", "b", "c"]);
console.log(x, y, z); // a,b,c
var {a, b} = {a: "aaa", b: "bbb"}; // 对象解构
console.log(a, b); // 输出 aaa 和 bbb
var [a, b, c, d, e] = 'hello'; // 字符串解构
console.log(a, b, c, d, e); // 分别输出 h、e、l、l、o
// 函数的解构
function add([x, y])
{
return x + y;
}
add([1, 2]); // 3
- 如果解构不成功,就会赋予默认值
undefined
。 - 解构赋值不仅适用于var命令,也适用于let和const命令。
- 只要某种数据结构具有
Iterator
接口,都可以采用数组形式的解构赋值,如Set
解构的用途
- 交换变量的值,如
[x, y] = [y, x];
- 从函数返回多个值,如:
function foo(){ return [1, 2, 3];} var [a, b, c] = foo();
- 提取JSON数据
- 给函数参数设置默认值
- 遍历map:
var map = new Map();
map.set('first', 'hello');
map.set('second', 'world');
for (let [key, value] of map)
{
console.log(key + " is " + value);
}
// first is hello
// second is world
字符串的扩展
传统的\uxxxx
字符表示法只能表示\u0000~\uFFFF
之间的字符,超出这个范围的字符会显示异常。ES6可以采用\u{xxxx}
的方式来表示超出范围的字符,如\u{20BB7}
。
本小节有待补充。
正则扩展
新增3个修饰符
在原有g
、i
、m
基础上再增加3个修饰符:
u
:Unicode 模式,用来正确处理大于\uFFFF
的 Unicode 字符;y
:“粘连”(sticky)修饰符,与g
类似,唯一区别:y
确保匹配必须从剩余的第一个位置开始,而g
只要剩余位置中存在匹配就可;s
:传统情况下,.
匹配除换行符以为任意字符。如果加了s
修饰符,则匹配包括换行符在内的任意字符。
支持后行断言
另外,JS中一直不支持的后行断言也在ES6中被支持。
具名组匹配
同时增加了具名组匹配,在此之前只能通过索引匹配组,圆括号里面的内容通过索引获取,例如:
/^(\d+?)-(\d+?)-(\d+)$/g.exec('2018-06-05'); // ["2018-06-05", "2018", "06", "05"]
es6新增?<name>
的方式来给组命名,例如:
var result = /^(?<year>\d+?)-(?<month>\d+?)-(?<day>\d+)$/.exec('2018-06-05');
console.log(result.groups.year);
console.log(result.groups.month);
console.log(result.groups.day);
数值的扩展
函数的扩展
- 新增函数默认值,例如:
function log(x, y = 'World') {}
; - 新增可变参数列表,形式为
...变量名
,又叫rest
参数,和Java非常类似,只能作为最后一个参数,与arguments
不同的是它是一个真正数组而不是伪数组; - 箭头函数,例如:
var fn = a => a*2
,主要优点是绑定this。
下面2个变化仅作为了解,不算特性:
- 函数的 length 属性变化,不计入默认参数和
rest
参数的个数; - 严格模式变化,只要函数参数使用了默认值、解构赋值、或者扩展运算符就不能主动设置
'use strict'
;
关于箭头函数:
- 箭头函数会绑定其所在作用域的
this
,其父作用域的this指向哪里,它本身的this就指向哪里。。 - 不可以当作构造函数,也就是说不能拿来
new
,否则报错; - 不可以使用
arguments
对象,该对象在函数体内直接不存在,如果非要使用,可以用rest
参数代替。 - 不可以使用
yield
命令,因此箭头函数不能用作Generator
函数。
数组的扩展
新增了一些方法,个人觉得比较常见的是...
扩展运算符以及Array.from
。
...
扩展运算符可以看成是rest
参数的逆运算,前者将数组拆散,后者将分散的变量合并为一个数组:
function fn(a, b, ...c) {console.log(a, b, c);}
fn(...[1, 2, 3, 4]); // 1, 2, [3, 4]
最常见用途是替代apply方法,ES6以前要将数组拆散传给函数执行只能使用fn.apply(this, [arg1, arg2])
,ES6直接可以fn(...[arg1, arg2])
常见示例:
- 求最大值,ES5:
Math.max.apply(null, [1,4,2])
,ES6:Math.max(...[1,4,2])
- 复制数组,ES5:
var b = [1,2,3].concat()
,ES6:var b = [...[1,2,3]]
; - 合并数组,ES5:
var b = [1,2,3].concat([4,5])
,ES6:var b = [1,2,3].push(...[4,5])
,或者var b = [...[1,2,3], ...[4,5]]
;
对象的扩展
属性简写
- 普通变量简写:
var a = 1; var obj = {a};
- 函数简写:
var obj = { fn(){} };
- get和set简写:
var obj = {
_test: 123,
get test() {return this._test},
set test(value) {this._test = value;}
}
属性名表达式
例如:var a = 'test'; var obj = {[a+'_abc']: 123};
新增的几个方法
Object.is(a, b)
,比较2个值是否相等,和===
的区别只有2个:+0
和-0
不相等,NaN
和NaN
相等;Object.assign(target, ...source)
,作用:将source的所有可枚举属性(包括Symbol
属性)复制到target并返回target,缺点是不能深拷贝;Object.setPrototypeOf(obj, prototype)
等价于obj.__proto__ = prototype
;Object.getPrototypeOf(obj)
等价于return obj.__proto__
;Object.values
,同ES5的Object.keys
类似,也是遍历自身可枚举属性,只不过返回的是value数组;Object.entries
,返回一个二维数组,形如[[key1, value1], [key2, value2]]
除此之外,ES6中,__proto__
被写入标准,浏览器环境必须部署这个属性,其它环境非必须,但是一般不推荐直接操作这个属性。
遍历和可枚举性
普通方式定义的属性都是可枚举的,要将某个属性设置成不可枚举必须使用Object.defineProperty
将enumerable
设置为false
。
5种遍历对象属性的方法:
for in
:遍历自身和原型上的可枚举属性(不含Symbol
属性);Object.keys(obj)
:遍历自身可枚举属性(不含原型上的,以及Symbol
属性);Object.getOwnPropertyNames(obj)
:遍历自身所有字符串属性,包括不可枚举(不含原型上的,以及Symbol
属性);Object.getOwnPropertySymbols(obj)
:遍历自身所有Symbol
属性,包括不可枚举(不含原型上的,以及字符串属性);Reflect.ownKeys(obj)
:遍历自身所有属性,包括Sybol
属性,包括不可枚举(不含原型上的);
可见,以上5种方法除了for in
会遍历原型上的方法需要特别记忆,其它方法都只遍历自身属性。
测试:
var obj = {a: 1, [Symbol('b')]: 2};
Object.prototype.c = 3; // 这里偷懒,直接修改Object的原型
Object.defineProperty(obj, 'd', {
value: 4,
enumerable: false
})
Object.defineProperty(obj, Symbol('e'), {
value: 5,
enumerable: false
})
for(var i in obj) console.log(i); // a c
Object.keys(obj); // ['a']
Object.getOwnPropertyNames(obj); // ['a', 'd']
Object.getOwnPropertySymbols(obj); // [Symbol(b), Symbol(e)]
Reflect.ownKeys(obj); // ['a', 'd', Symbol(b), Symbol(e)]
运行结果如下:
除此之外:
- JSON.stringify():只串行化对象自身的可枚举的属性;
- Object.assign():只遍历自身可枚举属性;
super关键字
this
代表当前对象,super
表示对象的原型,等价于obj.__proto__
,目前只能用在对象的简写方法中var obj = { fn(){return super.xxx;} };
,直接写对象里面、普通函数写法、或者箭头函数都报错:
// 报错
const obj = {
foo: super.foo
}
// 报错
const obj = {
foo: () => super.foo
}
// 报错
const obj = {
foo: function () {
return super.foo
}
}
对象的扩展运算符
我们已经知道数组有扩展运算符,ES2018
又将扩展运算符引入了对象,语法基本类似:
对象属性合并:
var { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
console.log(x, y, z); // 1, 2, {a: 3, b: 4}
对象属性拆解:
let a = {a: 3, b: 4}, b = {x:1, y: 2};
let target = {...a, ...b};
console.log(target); // {a: 3, b: 4, x: 1, y: 2},等价于 Object.assign({}, a, b)
Symbol
ES6新增一种原始数据类型Symbol
,表示独一无二的值。它是JS的第七种数据类型,前六种是:undefined
、null
、Boolean
、String
、Number
、Object
。我们可以把它大致看成是一种特殊的字符串,这种字符串是唯一的,没发重复生成的。
- 它由
Symbol
函数生成,接收一个可省的key参数,这个参数仅仅是用来标识,没其它用途。 - 每次都会生成一个唯一的值;
for in
、for of
等常规方法无法遍历Symbol属性,必须通过Object.getOwnPropertySymbols
或者Reflect.ownKeys
才能获取到。
var a = Symbol('a');
console.log(a); // Symbol(a)
typeof a; // 'symbol'
Symbol('a') === Symbol('a'); // false
Symbol('a').toString(); // 'Symbol(a)'
Symbol.for()
:首先会看全局环境有没有注册相同描述的Symbol值,如果有直接返回,否则创建一个新的Symbol并在全局环境注册,这个全局环境包括iframe。
大致原理如下(仅仅是模拟类似效果):
Sumbol.for = function(key) {
var result = window.symbolKeys[key] || Symbol(key);
window.symbolKeys[key] = result;
return result;
}
Symbol.keyFor(symbol)
:返回一个由Symbol.for
生成的Symbol的key值,如果是由Symbol()
生成的则返回undefined
。
除此之外,ES6 还提供了 11 个内置的 Symbol 值,指向语言内部使用的方法,这里只介绍Symbol.hasInstance
和Symbol.iterator
:
ES6以前,A instanceof B
的判断逻辑我们一般认为是闲着A的原型链往上找,如果匹配到了B就返回true,但是ES6新增一条规则:如果B定义了名为Symbol.hasInstance
的Symbol方法,就以这个方法返回的结果为准:
var Test = {
[Symbol.hasInstance](obj) {
return true; // 任何遍历和Test做instanceof比较都返回true
}
};
123 instanceof Test; // true
'abc' instanceof Test; // true
/a/g instanceof Test; // true
一个对象只要按照要求部署了名为Symbol.iterator
的方法就可以被for of
遍历,详见后文关于Iterator
部分。
Set和Map
Set
Set
是一种类似数组的新的数据结构,叫集合,最大特点是内容必须是唯一的。形式上Set
是一个构造函数,接收一个可选的数组参数初始化,通过add
方法添加内容,delete
方法删除内容。
属性:
size
:返回成员的个数,注意set获取长度是size而不是length,而且size是一个属性而不是一个方法;
方法:
set.add(value)
:添加值,如果已存在则不添加,返回自身,所以add()
可以链式调用,Set判断重复的逻辑和===
基本类似,唯一区别是2NaN
相等(注意和Object.is(a, b)
也有差别,+0
和-0
会被认为是同一个值);set.delete(value)
:删除某个内容,返回是否删除成功;set.has(value)
:判断set中是否存在某个value;set.clear()
:清空set;
示例:
var set = new Set([1, 2, 3]); // 参数为可选
set.add(4).add(5); // 可以链式调用
set.add(4); // 故意添加一个重复元素
set.size; // 4,
遍历(顺序就是插入顺序):
set.keys()
:返回键名的遍历器,Set的键值比较特殊,它的key其实就是value,所以set.keys()
和set.values()
效果完全相同。set.values()
:返回键值的遍历器;set.entries()
:返回所有成员的遍历器,内容为[value, value]
(因为key和value相同)。set.forEach()
:Set
也可以像数组一样用forEach遍历set.forEach((value, key) => {});
;for of
:Set
内部已经实现了迭代器接口,所以可以使用for of
遍历,也可以用...
进行扩展,[...new Set(array)]
是最简单的数组去重方法。
一般用for of
遍历就足够了,其实for of
内部调用的就是set.values()
。
Map
ES6新增一种名叫Map的数据结构,类似于对象,对象为字符串->值
的映射,而Map为值->值
的映射,也就是Map的key可以是任意对象,而不仅仅是字符串。Map通过set添加值,get取值,可以接收一个形如[[key1, value1], [key2, value2]]
的二维数组来初始化。
var map = new Map([
['name', 'tom'],
['age', 18]
]);
var key = {a: 1};
map.set(key, 'aaa'); // 对象做键值
map.get(key); // 'aaa'
属性:
size
:返回成员的个数;
方法:
map.set(key, value)
:设置值,如果已经存在则覆盖;map.get(key)
:取值,不存在则返回undefined
;map.delete(key)
:删除某个key,返回是否删除成功;map.has(key)
:判断map中是否存在某个key;map.clear()
:清空map;
遍历(顺序为插入顺序):
map.keys()
:返回键名的遍历器。map.values()
:返回键值的遍历器。map.entries()
:返回所有成员的遍历器。map.forEach()
:遍历Map
的所有成员。for of
:一般格式为:for (let [key, value] of map) {}
;
与数组互转:
- 二维数组转Map:
new Map([二维数组)
; - Map转二维数组:
[...map.entries()]
;
WeakSet和WeakMap
WeakSet
根据字面意思理解为弱集合
,WeakSet
与Set
基本类似,只有2个区别:
- 只能存放对象,存放其它类似会报错;
WeakSet
对它里面的对象都是弱引用,GC回收时不考虑WeakSet
对它的引用,也就是,只要这个对象没有被其它对象引用GC就会回收它。
所以它里面的内容随时可能消失,也因此它没有size
属性,而且它不能遍历。
WeakMap
和WeakSet
类似,也是键值只能是对象,弱引用,这里不再详述。
Proxy
Proxy
代理,作用是给目标对象设置拦截器,从语言层面拦截一些默认行为。
语法:
// target:要拦截的对象
// handler:拦截器,也是一个对象,用来定制拦截行为
var obj = new Proxy(target, handler);
下面的实例为拦截对象的get方法:
var obj = new Proxy({}, {
get: function(target, property) {
return '小茗同学很帅';
}
});
obj.time; // '小茗同学很帅'
obj.name; // '小茗同学很帅'
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)
。
Reflect
Reflect
,中文含义是反射
,熟悉Java
的一听到这个词应该知道这个大概是个什么东西了。它是一个普通对象,下面放置了一些和语言层面关联较大的静态方法,其设计目的主要是:
- 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上;
- 让Object操作都变成函数行为;
- Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。前面说了,Proxy一共可以拦截13种默认行为,所以Reflect下面也有13个静态方法。
这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)
Promise
Promise
是异步编程的一种解决方案,其本质可以看成是一个状态机,一个容器,里面保存着某个未来才会执行的操作。Promise
实例有3种状态:pending
、resolved
、rejected
,状态不可逆。
Promise
是一个构造函数,接收一个函数作为参数:new Promise(function(resolve, reject){});
,这个函数有2个参数,resolve()
方法将状态从pending
变为resolved
,reject()
方法将pending
变为rejected
,new
完Promise
之后这个函数会立即执行。
下文中,小写的promise
代表Promise
实例,大写的Promise
表示原始对象。
promise.then
:为promise实例添加状态改变的回调函数promise.then(success, failed)
,第二个失败回调可以省略,返回一个新的promise实例;promise.catch
:它是.then(null, failed)
的别名,promise的错误会被最近的一个catch捕获;Promise.all([promiseList])
:将多个promise对象包装成一个新的promise对象,只有所有promise
实例状态都变成resolved
才会触发成功回调(参数是所有实例结果组成的一个新数组),只要有一个实例状态变成rejected
就会触发失败回调。Promise.race([promiseList])
:哪个实例对象状态最先发生改变就以哪个为准,即使状态是变成rejected
;Promise.resolve()
:将某个对象转化成promise实例,立即返回一个状态为resolved的promise对象。特别注意,Promise.resolve()
在本轮“事件循环”结束时执行,而setTimeout(fn, 0)
在下一轮“事件循环”开始时执行。Promise.reject()
:立即返回一个状态为rejected
的promise对象,与Promise.resolve
不同的是,其参数会原封不动地作为reject的理由,变成后续方法的参数。
可以发现,除了promise.then
、Promise.all
、Promise.race
这3个方法之外,其余的都是语法糖,可以通过其它方式变相实现。
特别注意:通过new Promise()
返回的promise实例,其回调函数里面必须调用resolve或者reject方法,否则then永远不会被触发,但是,通过then返回的promise实例,即使回调函数不返回任何内容,新实例的then也会被触发,因为其状态默认就是resolved。
new Promise(function(resolve, reject) {}).then(data => console.log(data)); // then方法永远不会执行
new Promise(function(resolve, reject) {
resolve(123);
}).then(data => console.log('第一个then', data))
.then(data => console.log('第二个then', data)); // 2个then都会被触发
Iterator
迭代器Iterator
(也叫遍历器)是一种接口,为各种不同的数据结构提供统一的访问机制,主要是供ES6新增的for of
使用。迭代器本质上可以看成是一个指针对象,指向当前迭代的索引。
一个数据结构只要具有Symbol.iterator
属性(原型上有也可以),就可以认为是可遍历的(iterable
)。Symbol.iterator
必须是一个函数,返回一个对象(也就是前面说的迭代器),这个对象必须要有一个next方法,返回类似{value:1, done: false}
的内容(done为true表示遍历完毕)。
数组、类数组(如arguments
、NodeList
等)、Map
、Set
等自带Symbol.iterator
属性,所以这些数据结构默认就可以使用for of
遍历。对象默认不能使用for of
遍历,因为它没有部署Symbol.iterator
属性,之所以没有部署主要有2个原因,一是因为对象属性的顺序是不确定的,需要开发者自行指定,二是因为对于需要遍历的场景,Map完全可以替代Object,所以,为Object部署迭代器属性不是很必要。
让普通对象可以遍历:
var obj = {a:1, b:2, c: 3};
obj[Symbol.iterator] = function() {
var keys = Object.keys(this), idx = 0;
return {
next: () => {
return idx < keys.length ? {value: [keys[idx], this[keys[idx++]]]} : {done: true};
}
}
}
// 测试
for(let [key, value] of obj) console.log(key, value);
遍历器对象除了具有next
方法,还可以具有return
方法和throw
方法。return
方法主要在break
、continue
、throw
这几种场合被调用。throw
方法主要是配合Generator
函数使用,详见后文。
调用Iterator的场合
除了for of
之外,还有一些地方会用到迭代器,如:
- for of
- 扩展运算符
...
,如[...array]
- 解构赋值:对数组和 Set 结构进行解构赋值,如
var [a, b, c] = new Set([1, 2, 3])
yield*
:如yield* [2,3,4]
- 使用数组做参数的场合,如:
Array.from()
、Promise.all()
、new Set()
等;
Generator语法
Generator
函数是ES6
提供的又一种异步编程解决方案(前一种是Promise
)。