蜡笔小新语录: 总是生活中有许多无奈只要你坚持不懈,你会发现你会更无奈。
作者希冀: 你们是否在被割韭菜,希望你们躺赢!
ES2015是ECMAScript 2015的简称,ECMA又是欧洲计算机制造者协会的简称,
ECMAScript代表 JavaScript 这门语言所遵循的规范。
2011 年,ECMAScript 5.1 版发布后,就开始制定 6.0 版了,
直到2015年ES6.0才正式出炉。
因此,ES6 这个词的原意,就是指 JavaScript 语言在ES5.1后的下一个版本ES6.0。
深入学习ES6相关API
Symbol
ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。
它是 JavaScript 语言的第七种数据类型,
前六种是:undefined、null、Boolean、String、Number、Object
1 . 直接调用Symbol函数即可生成一个Symbol,注意Symbol函数前不能使用new命令,否则会报错。
2 . Symbol函数可以接受一个字符串作为参数,表示对 Symbol 的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。
let s1 = Symbol('foo');
let s2 = Symbol('bar');
s1 // Symbol(foo)
s2 // Symbol(bar)
注意,Symbol函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的Symbol函数的返回值是不相等的。
// 没有参数的情况
let s1 = Symbol();
let s2 = Symbol();
s1 === s2 // false
// 有参数的情况
let s1 = Symbol('foo');
let s2 = Symbol('foo');
s1 === s2 // false
3 . Symbol 值可以作为标识符,用于对象的属性名,由于每一个 Symbol 值都是不相等的。这意味着就能保证不会出现同名的属性,能防止某一个键被不小心改写或覆盖的情况。
注意,在对象的内部,使用 Symbol 值定义属性时,Symbol 值必须放在方括号之中。
let mySymbol = Symbol();
// 第一种写法
let a = {};
a[mySymbol] = 'Hello!';
// 第二种写法
let a = {
[mySymbol]: 'Hello!'
};
// 第三种写法
let a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });
// 以上写法都得到同样结果
a[mySymbol] // "Hello!"
tip: Symbol 作为属性名,该属性不会出现在for...in、for...of循环中也不被Object.keys()、Object.getOwnPropertyNames()、JSON.stringify()返回。但是,有一个Object.getOwnPropertySymbols方法,可以获取指定对象的所有 Symbol 属性名。
Symbol.for
有时,我们希望重新使用同一个 Symbol 值,Symbol.for方法可以做到这一点。
它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值
如果有,就返回这个 Symbol 值,否则就新建并返回一个以该字符串为名称的 Symbol 值。
let s1 = Symbol.for('foo');
let s2 = Symbol.for('foo');
s1 === s2 // true
内置Symbol
除了定义自己使用的 Symbol 值以外,ES6 还提供了 11 个内置的 Symbol 值,指向语言内部使用的方法。
Symbol.hasInstance
Symbol.isConcatSpreadable
Symbol.species
Symbol.match
Symbol.replace
Symbol.search
Symbol.split
Symbol.toPrimitive
Symbol.toStringTag
Symbol.unscopables
Symbol.iterator,对象的Symbol.iterator属性,指向该对象的默认生成遍历器的方法。
Set和WeakSet
ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。
需要记录不同成员的又不希望重复记录的情况下可以用到Set
如何生成set
let set1 = new Set()
let set2 = new Set([1,2,3])
Set 实例的属性:
Set.prototype.size:返回Set实例的成员总数。
Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。
四个操作方法:
- Set.prototype.add(value):添加某个值,返回 Set 结构本身。
- Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
- Set.prototype.has(value):返回一个布尔值,表示该值是否为Set的成员。
- Set.prototype.clear():清除所有成员,没有返回值。
四个遍历方法:
- Set.prototype.keys():返回键名遍历器
- Set.prototype.values():返回键值遍历器
- Set.prototype.entries():返回键值对遍历器
- Set.prototype.forEach():使用回调函数遍历每个成员
注意:Set实例中key和value是一样的,所以keys()和values()这两个方法的结果是一样的
WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。
-
WeakSet的成员只能是对象,而不能是其他类型的值。 -
WeakSet中的对象都是弱引用
如果一个对象没有任何引用,那么此对象会尽快被垃圾回收,释放掉它占用的内存。
即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。
WeakSet 结构有以下三个方法。
WeakSet.prototype.add(value):向 WeakSet 实例添加一个新成员。WeakSet.prototype.delete(value):清除 WeakSet 实例的指定成员。WeakSet.prototype.has(value):返回一个布尔值,表示某个值是否在WeakSet实例之中。
WeakSet 不能遍历,是因为成员都是弱引用,随时可能消失。
示例:
let div = document.querySelector('div')
let set = new Set()
set.add(div)
//...some code
document.body.removeChild(div)
div = null //dom对象仍在内存中,因为Set中仍然引用此对象
let div = document.querySelector('div')
let weakset = new WeakSet()
weakset.add(div)
//...some code
document.body.removeChild(div)
div = null //dom对象的已经没有引用,将被垃圾回收机制回收
Map和WeakMap
Map
JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。
为了解决这个问题,ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键,是一种更完善的 Hash 结构实现。
生成Map实例:
const map1 = new Map();
const map2 = new Map([
['name', '张三'],
['title', 'Author']
]);
Map 实例的属性:
Map.prototype.size:返回Map实例的成员总数。
Map实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。
四个操作方法:
Map.prototype.set(key,value):设置键名key对应的键值为value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键。Map.prototype.get(key):读取key对应的键值,如果找不到key,返回undefined。Map.prototype.has(key):返回一个布尔值,表示某个键是否在当前 Map 对象之中。Map.prototype.delete(key):删除某个键,返回true。如果删除失败,返回false。Map.prototype.clear():清除所有成员,没有返回值。
四个遍历方法:
Map.prototype.keys():返回键名遍历器Map.prototype.values():返回键值遍历器Map.prototype.entries():返回键值对遍历器Map.prototype.forEach():使用回调函数遍历每个成员
实例1:扩展对象
当我们有一系列对象,想记录每个对象一种属性。假设有100只鸡,需要记录每只鸡的重量,有两种思路:
- 想办法用笔写到鸡身上
- 记录到一个本本上
class Chicken {
}
// 100只鸡
let chickenList = []
for (let i = 0; i < 100; i++) {
chickenList.push(new Chicken())
}
// 方法1:记录到鸡身上
chickenList.forEach(function(chicken, index){
chicken.weight = getWeight(chicken);
});
// 方法2:记录到本本上
let notebook = [];
chickenList.forEach(function(chicken, index){
notebook[index] = getWeight(chicken);
});
第1种思路存在以下问题:
- 破坏了鸡的卖相,有时候这是很严重的事情,比如你想把一只5斤的鸡当成6斤卖出去,结果鸡身上直接写“我只有5斤”(修改了原有对象,可能导致意外的行为)
- 可能碰到一些战斗鸡,一个字都写不上去(对象冻结了或者有不可覆盖的属性)
- 可能写到一些本来就写了字的地方,导致根本看不清(与对象原有属性冲突)
再看第2种方法,存在以下问题:
- 本本无法和鸡精准地一一对应,只能靠一些索引或者标记(例如给每只鸡起一个名字)去(不可靠)地记录对应关系(无法精准地对比到是哪一个对象)
这时候就可以使用Map扩展对象
// 记录到另一个本本上
let notebook = new Map();
chickenList.forEach(function(chicken, index){
notebook.set(chicken, getWeight(chicken));
});
实例2:完善私有属性的实现
回顾之前的Symbol实现的私有属性的版本里,仍然存在着可以被特殊api遍历的缺陷。
基于Map的解决思路:
用一个闭包内的Map来扩展每个生成的对象
var Person = (function() {
var map = new Map();
function Person(name) {
map.set(this,name);
}
Person.prototype.getName = function() {
return map.get(this);
};
return Person;
}());
WeakMap
与之前介绍的WeakSet 类似,WeakMap与 Map 有两个区别。
WeakMap的键只能是对象,而不能是其他类型的值。WeakMap中对键的引用是弱引用
同样地,WeakMap 不能遍历,是因为成员都是弱引用,随时可能消失。
WeakMap只有四个方法可用:get()、set()、has()、delete()。
注意:WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。
const wm = new WeakMap();
let key = {};
let obj = {foo: 1};
wm.set(key, obj);
obj = null;
wm.get(key)
实例:完善私有属性的实现
前面基于Map的实现还存在一个问题:
当Person实例的外部引用消除时,闭包中的Map仍然有Person实例作为键的引用,Person实例不会被垃圾回收,必须等到所有的Person实例的外部引用消除,Map所在的闭包也会消除,最后Person实例才会被垃圾回收
为了解决这个问题,使用WeakMap进一步完善:
var Person = (function() {
var wm = new WeakMap();
function Person(name) {
wm.set(this,name);
}
Person.prototype.getName = function() {
return wm.get(this);
};
return Person;
}());
Proxy
在ES6之前Object.defineProperty可以拦截对象属性的读取和修改操作,Proxy 可以理解成比这个API更强大的,在目标对象之前架设一层的“拦截”。外界对该Proxy对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。
注意:只有对生成的 Proxy 实例操作才能起到拦截的作用
生成Proxy实例:
var proxy = new Proxy(target, handler);
- target :需要代理的对象
- handler :拦截函数的集合
如果handler是空对象则代表任何操作都不会拦截
let obj = {}
/*handler为空对象*/
let proxy = new Proxy(obj, {});
proxy.a = 1
//obj.a //1
对属性的读取进行拦截:
var proxy = new Proxy({}, {
get: function(target, property) {
return 35;
}
});
proxy.time // 35
proxy.name // 35
proxy.title // 35
下面是 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),返回一个布尔值。 - getPrototypeOf(target):拦截
Object.getPrototypeOf(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)。 - isExtensible(target):拦截
Object.isExtensible(proxy),返回一个布尔值。 - preventExtensions(target):拦截
Object.preventExtensions(proxy),返回一个布尔值。
Proxy给了开发者拦截语言默认行为的权限,可以不改变原有对象或函数的情况下,轻松运用在很多场景。例如:统计函数调用次数,实现响应式数据观测(Vue 3.0),实现不可变数据(Immutable)等等
Reflect
Reflect是 ES6 为了操作对象而提供的新 API。ES6把原先版本中很多语言层面的API,比如Object.defineProperty delete in等集中在了Reflect的静态方法上,引入Reflect的目的有这样几个。
(1) 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。现阶段,某些方法同时在Object和Reflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法。
(2) 修改某些Object方法的返回结果,让其变得更合理。
// 老写法
try {
Object.defineProperty(target, property, attributes);
// success
} catch (e) {
// failure
}
// 新写法
if (Reflect.defineProperty(target, property, attributes)) {
// success
} else {
// failure
}
(3)将命令式操作转变为函数调用,避免更多的保留字占用。比如name in obj和delete obj[name],对应Reflect.has(obj, name)和Reflect.deleteProperty(obj, name)
// 老写法
'assign' in Object // true
// 新写法
Reflect.has(Object, 'assign') // true
(4)Reflect对象的方法与Proxy对象的方法一一对应,想要调用默认行为,直接在Reflect上调用同名方法,简单可靠,省去人工写默认行为的代码。
let proxy = new Proxy({}, {
set: function(target, name, value, receiver) {
var success = Reflect.set(target, name, value, receiver);
if (success) {
console.log('property ' + name + ' on ' + target + ' set to ' + value);
}
return success;
}
});
Reflect对象一共有 13 个静态方法。
- Reflect.apply(target, thisArg, args)
- Reflect.construct(target, args)
- Reflect.get(target, name, receiver)
- Reflect.set(target, name, value, receiver)
- Reflect.defineProperty(target, name, desc)
- Reflect.deleteProperty(target, name)
- Reflect.has(target, name)
- Reflect.ownKeys(target)
- Reflect.isExtensible(target)
- Reflect.preventExtensions(target)
- Reflect.getOwnPropertyDescriptor(target, name)
- Reflect.getPrototypeOf(target)
- Reflect.setPrototypeOf(target, prototype)
上面这些方法的作用,与Proxy对象handler的方法是一一对应的。
Iterator
Iterator(遍历器、迭代器) 是一个对象,Iterator对象需要包含一个next方法,该方法返回一个对象,此对象有两个属性,一个value表示当前结果,一个done表示是否可以继续迭代
let it = makeIterator();
function makeIterator() {
let nextIndex = 0;
return {
next: function() {
return nextIndex < 5 ?
{value: nextIndex++, done: false} :
{value: undefined, done: true};
}
};
}
ES6 规定,如果数据结构的Symbol.iterator属性是一个方法,该方法返回Iterator对象,就可以认为此数据结构是“可遍历的”(iterable)
interface Iterable {
[Symbol.iterator]() : Iterator,
}
interface Iterator {
next(value?: any) : IterationResult,
}
interface IterationResult {
value: any,
done: boolean,
}
实例:
let obj = {
[Symbol.iterator]:makeIterator
}
ES6中以下场合会默认调用 Iterator 接口(即Symbol.iterator方法),
for...of循环- 数组解构
- 扩展运算符
yield*- 其他隐式调用的地方,例如
new Set(['a','b']),Promise.all()等
ES6中以下数据结构默认为可遍历对象,即默认部署了Symbol.iterator属性
- Array
- Map
- Set
- String
- 函数的 arguments 对象
- NodeList 对象
Generator函数
基本概念
Generator(生成器) 函数是 ES6 提供的一种异步编程解决方案,并且Generator函数的行为与传统函数完全不同。
定义Generator函数
function* f() {
}
形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部可以使用yield关键字,定义不同的内部状态(yield在英语里的意思就是“产出”)。
执行Generator函数
执行 Generator 函数,函数本身不会执行,而是会返回一个遍历器对象,同时该对象也是可遍历的,因为在其原型链上也具有Symbol.iterator方法,并且改方法返回的对象就是该遍历器对象自身
function* f() {
console.log(1)
}
let a = f()
a[Symbol.iterator]() === a // true
因此,Generator函数返回的对象也可以被遍历,相当于每次调用此对象next()的value来作为遍历结果
只有执行了该遍历器对象的next()方法,Generator函数才会执行:
function* f() {
console.log(1)
}
let a = f()
a.next() // 打印1 返回{value:undefined,done:true}
yield 和 yield*
Generator函数中可以使用yield关键字来定义函数返回的遍历器对象每次next()后的value
function* f() {
yield 1
}
let a = f()
a.next() // 返回 {value: 1, done: false}
并且a每次执行next(),都会在下一个yield处暂停,直到后面没有yield关键字,则执行剩余代码,并且返回done:true:
function* f() {
console.log('step1')
yield 1
console.log('step2');
yield 2
console.log('step3');
}
let a = f()
a.next() // 打印step1 返回 {value: 1, done: false}
a.next() // 打印step2 返回 {value: 2, done: false}
a.next() // 打印step3 返回 {value: undefined, done: true}
yield本身没有返回值,yield的返回值是下一次next()函数传入的值。
所以next()方法的作用有两个
- 执行本次
yield到下一个yield之间的代码 - 将形参的值传给本次
yield的返回值
next()和yield实现了函数内外控制权的转移。
function* f() {
console.log('start');
let result = yield 1
console.log('result:',result);
}
let a = f()
yield* 等同于遍历某个对象,并且yield每个结果
function* f() {
yield 'start'
yield* [1, 2, 3]
/*等同于*/
// for(let value of [1,2,3]){
// yield value
// }
yield 'end'
}
let a = f()
a.next() // 返回 {value: 'start', done: false}
a.next() // 返回 {value: 1, done: false}
a.next() // 返回 {value: 2, done: false}
a.next() // 返回 {value: 3, done: false}
a.next() // 返回 {value: 'end', done: false}
a.next() // 返回 {value: undefined, done: true}
Generator函数配合自动执行器
直接循环存在的问题
Generator函数是一种新的异步编程解决方案,但是每次手动调用next()很麻烦,如果我们写一个循环来执行next()呢?
function* f() {
yield 1
console.log('完成1');
yield 2
console.log('完成2');
}
let it = f()
let done = false
while (!done){
done = it.next().done
}
看似是没有问题,但是如果yield后面本身就是一个异步操作,就会有问题
function* f() {
yield readFile(file1)
console.log('耶,完成了1');
yield readFile(file2)
console.log('耶,完成了2');
}
let it = f()
let done = false
while (!done){
done = it.next().done
}
//耶,完成了1
//耶,完成了2
如果我们的需求是让异步操作执行完毕后再执行yield后面的代码,那么上述执行顺序就不符合需求。验证:
function* f() {
yield readFile(file1,function (err,data) {
console.log('读取到数据1:' + data)
})
console.log('耶,完成了1');
yield readFile(file2,function (err,data) {
console.log('读取到数据2:' + data)
})
console.log('耶,完成了2');
}
let it = f()
let done = false
while (!done){
done = it.next().done
}
//耶,完成了1
//耶,完成了2
//读取到数据1:111
//读取到数据2:222
Thunk函数
在 JavaScript 语言中,Thunk 函数是指将多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数。
// Thunk版本的readFile(单参数版本)
const {readFile} = require('fs')
const path = require('path')
const file1 = path.join(__dirname,'./text/1.txt')
const file2 = path.join(__dirname,'./text/2.txt')
let Thunk = function (fileName) {
return function (callback) {
return readFile(fileName, callback);
};
};
let readFileThunk = Thunk(file1);
readFileThunk(function(err,data){
console.log(String(data));
});
有一个
thunkify库可以方便的将api变成Thunk函数
自动执行器
写一个自动执行器run函数,每次将it.next()的逻辑封装到nextStep()中,并且将nextStep作为回调函数传给Thunk化后的读取文件函数。
// Thunk版本的readFile(单参数版本)
const {readFile} = require('fs')
const path = require('path')
const file1 = path.join(__dirname, './text/1.txt')
const file2 = path.join(__dirname, './text/2.txt')
let Thunk = function (fileName) {
return function (callback) { //result.value
return readFile(fileName, callback);
};
};
function* f() {
let data1 = yield Thunk(file1)
console.log('耶,完成了1,数据是' + data1);
let data2 = yield Thunk(file2)
console.log('耶,完成了2,数据是' + data2);
}
function run(f) {
let it = f();
function nextStep(err, data) {
var result = it.next(data);
if (result.done) return;
result.value(nextStep); //执行readFile,并且把nextStep作为回调传入
}
nextStep();
}
run(f)
如此一来,基于自动执行器,只要异步操作是Thunk函数或者返回Promise的情况下,写异步逻辑在形式上就如同写同步逻辑一样,非常简洁。
co模块
co模块是对一个封装的更好的自动执行器,它支持yield的类型,不光包含thunk函数,还有Promise对象,数组,对象,甚至Generator函数
const { promisify } = require("util");
const path = require('path')
const file1 = path.join(__dirname, './text/1.txt')
const file2 = path.join(__dirname, './text/2.txt')
const readFileP = promisify(readFile)
let Thunk = function (fileName) {
return function (callback) { //result.value
return readFile(fileName, callback);
};
};
/*Thunk*/
function* f() {
let data1 = yield Thunk(file1)
console.log('耶,完成了1,数据是' + data1);
let data2 = yield Thunk(file2)
console.log('耶,完成了2,数据是' + data2);
}
/*Promise*/
function* f() {
let data1 = yield readFileP(file1)
console.log('耶,完成了1,数据是' + data1);
let data2 = yield readFileP(file2)
console.log('耶,完成了2,数据是' + data2);
}
/*数组(并发)*/
function* f() {
let data = yield [readFileP(file1),readFileP(file2)]
console.log('耶,完成了,数据是' + data);
}
/*对象(并发)*/
function* f() {
let data = yield {data1:readFileP(file1),data2:readFileP(file2)}
console.log('耶,完成了,数据是' + JSON.stringify(data));
}
/*Generator函数*/
function* f() {
function* f1() {
return yield {data1:readFileP(file1),data2:readFileP(file2)}
}
let data = yield f1()
console.log('耶,完成了,数据是' + JSON.stringify(data));
}
co(f)
经过一个co模块执行后的Generator函数会返回一个Promise对象:
co(f).then(()=>{
console.log('co执行完毕');
})
async函数
基本概念
async 函数是什么?一句话,它就是 Generator 函数的语法糖。
将上一章的代码改成 async 函数的版本:
const { promisify } = require("util");
const path = require('path')
const file1 = path.join(__dirname, './text/1.txt')
const file2 = path.join(__dirname, './text/2.txt')
const readFileP = promisify(readFile)
function* f() {
let data1 = yield readFileP(file1)
console.log('耶,完成了1,数据是' + data1);
let data2 = yield readFileP(file2)
console.log('耶,完成了2,数据是' + data2);
}
//async函数的版本
async function f() {
let data1 = await readFileP(file1)
console.log('耶,完成了1,数据是' + data1);
let data2 = await readFileP(file2)
console.log('耶,完成了2,数据是' + data2);
}
比较后就会发现,async函数的版本就是将 Generator 函数的星号(*)替换成async,将yield替换成await。
定义async函数
使用async关键字定义一个async函数:
async function f() {
let data1 = await readFileP(file1)
console.log('耶,完成了1,数据是' + data1);
let data2 = await readFileP(file2)
console.log('耶,完成了2,数据是' + data2);
}
执行async函数
执行async函数则相当于执行了一个自动运行的Generator函数,async函数如果返回的结果不是Promise,则会运行结果包装成一个Promise返回:
async function f() {
console.log(1);
}
f().then(()=>{
console.log(2);
})
async function f() {
console.log(1);
return 'done'
}
f().then(value => {
console.log(value);
})
await关键字
与yield类似,async函数中可以使用await关键字,await关键字后面一般会写一个Promise实例,async函数执行的过程中,每次遇到await关键字,会将控制权转回外部环境。
- 如果
await后面是Promise实例,则会等到该 Promise实例被resolve后,才会把本次await到下次await之间的代码推到MircoTask(微任务)中等待执行,并且await的返回值是该Promise实例resolve的值 - 如果
await后面不是Promise实例,则会立即将本次await到下次await之间的代码推到MircoTask(微任务)中等待执行,并且await的返回值是等于await后面表达式的值:
async function f() {
let data = await new Promise((resolve, reject) => {
setTimeout(() => {
resolve('a')
}, 2000)
})
console.log(data);
}
//f()
//console.log('end')
如果await后面不是Promise 实例
async function f() {
let data = await 'a'
console.log(data);
}
f()
console.log('end');
//end
//a
async函数的错误处理
如果Promise被reject或抛出错误,await之后的代码不会执行,因此,需要使用try..catch对await进行错误捕捉:
async function f() {
try {
let data = await new Promise((resolve, reject) => {
setTimeout(() => {
reject('123')
}, 2000)
})
//后续代码无法执行
console.log('done');
}catch (e) {
console.log('发生错误:',e);
}
}
f()
async函数处理并发异步任务
如果,async函数中的每个await都是等到前面await resolve后才会执行,如果想并发执行,可以使用Promise.all:
/*并发处理异步*/
async function f() {
let time1 = new Date()
let [data1,data2] = await Promise.all([
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('123')
}, 2000)
}),
new Promise((resolve, reject) => {
setTimeout(() => {
resolve('123')
}, 3000)
})
])
console.log(data1,data2,'用时:'+ (new Date() - time1));
}
f()
async函数与Promise的对比
用async函数写异步逻辑相比Promise会更加简洁,在处理不同异步结果相互依赖,错误处理,if...else分支等情况时更加简便:
const {readFile} = require('fs')
const { promisify } = require("util");
const path = require('path')
const file1 = path.join(__dirname, './text/1.txt')
const file2 = path.join(__dirname, './text/2.txt')
const file3 = path.join(__dirname, './text/3.txt')
const readFileP = promisify(readFile)
function f1() {
readFileP(file1).then(data1 =>{
console.log('耶,完成了1,数据是' + data1);
return readFileP(file2)
}).then(data2 => {
console.log('耶,完成了1,数据是' + data2);
return readFileP(file3)
}).then(data3 => {
console.log('耶,完成了1,数据是' + data3);
})
}
async function f2() {
let data1 = await readFileP(file1)
console.log('耶,完成了1,数据是' + data1);
let data2 = await readFileP(file2)
console.log('耶,完成了2,数据是' + data1 + data2);
let data3 = await readFileP(file3)
console.log('耶,完成了2,数据是' + data1 + data2 + data3);
}
f()