1. Promise
Promise对象有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。一旦状态改变,就不会再变。
Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。
如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
1.1 Promise 的状态如何改变
const promise = new Promise(function(resolve, reject) {
// ... some code
if (/* 异步操作成功 */){
resolve(value);
} else {
reject(error);
}
});
new Promise()时,会立即执行传进来的exector函数,并向exector函数传递两个参数,一般命名是resolve,reject它们是两个函数,由JavaScript引擎提供,不用自己部署。
resolve:将Promise对象的状态从pending变为resolved,在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject:将Promise对象的状态从pending变为rejected,在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去;- 如果
exector函数执行报错,则会将Promise对象的状态从pending变为rejected,并将报错信息传递出去; - A的状态由B决定。 对于第4点,调用resolve/reject函数时带有参数,那么它们的参数会被传递给回调函数。resolve/reject函数的参数除了正常的值以外,还可能是另一个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方法指定的回调函数。
另外,不同的调用会导致不同的结果:
const p1 = new Promise(function (resolve, reject) {
> setTimeout(() => resolve(999), 200)
})
const p2 = new Promise(function (resolve, reject) {
setTimeout(() => resolve(p1), 10)
})
p2
.then(result => console.log(result, 'resolve')) // 999 "resolve"
.catch(error => console.log(error, 'reject'))
const p1 = new Promise(function (resolve, reject) {
setTimeout(() => resolve(999), 200)
})
const p2 = new Promise(function (resolve, reject) {
> setTimeout(() => reject(p1), 10)
})
p2
.then(result => console.log(result, 'resolve'))
.catch(error => console.log(error, 'reject'))
/* 输出
Promise {<pending>}
__proto__: Promise
[[PromiseState]]: "fulfilled"
[[PromiseResult]]: 999
"reject"
**/
const p1 = new Promise(function (resolve, reject) {
> setTimeout(() => reject(999), 200)
})
const p2 = new Promise(function (resolve, reject) {
setTimeout(() => reject(p1), 10)
})
p2
.then(result => console.log(result, 'resolve'))
.catch(error => console.log(error, 'reject'))
/**
Promise {<pending>}
__proto__: Promise
[[PromiseState]]: "rejected"
[[PromiseResult]]: 999
"reject"
**/
由此得出结论:
- 当
p2以resolve(p1)来改变状态,则p2的状态由p1决定,若p1状态为pending,则会等待p1状态改变之后,p2的状态才会改变。并且传递的值为p1的resolve/reject的参数。 - 当
p2以reject(p1)来改变状态,那么p2的状态由自己决定,并且传递的值为p1对象。
1.2 Promise.prototype.then
then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。
那么新Promise实例的状态和值,由谁来决定?看下面这个例子:
let p1 = new Promise((resolve, reject) => {
reject(-100);
});
let p2 = p1.then(result => {
console.log(`成功:${result}`);
return 200;
}, reason => {
console.log(`失败:${reason}`); // 失败:-100
return -200;
});
let p3 = p2.then(result => {
console.log(`成功:${result}`); // 成功:-200
throw new Error('xxxx')
}, reason => {
console.log(`失败:${reason}`);
});
p3.then(result => {
console.log(`成功:${result}`);
}, reason => {
console.log(`失败:${reason}`); //失败:Error: xxxx
});
不论p1是fulfilled或是rejected,我们只看p1.then执行是否报错;如果报错则p2的状态是失败rejected,值是报错信息;如果不报错则p2的状态是成功fulfilled,值是函数的返回值。
此概念可以借助try/catch的异常处理机制来理解,Promise.prototype.then(resolution, rejection)等价于Promise.prototype.then().catch(),这其实就是异常的处理过程,异常一层层向上抛出(Promise是向后冒)。
- 若
p1状态为fulfilled,则执行then函数,then执行完,返回的新实例状态为fulfilled。 - 若
p1状态为rejected,则执行catch函数,也就是异常处理函数,异常在catch中被处理掉,就没有异常了,故返回的新实例状态也是fulfilled。 - 若在执行
then/catch函数时再次发生异常,则异常向上抛出,返回的新实例状态为rejected,若该新实例有then/catch处理函数,则按上面的规则处理,若没有,则错误不会传递到外层代码。 - 《ES6》:跟传统的
try/catch代码块不同的是,如果没有使用catch()方法指定错误处理的回调函数,Promise对象抛出的错误不会传递到外层代码,即不会有任何反应。 所以无论p1状态如何,只要在执行then/catch的过程中没有代码错误,那么返回的新实例状态为fulfiled。
新实例的状态还有另一种情况:执行不报错,但是返回的值是另一个Promise实例,这时新实例的状态会由该Promise对象的状态决定。 值是该Promise的值。
下面有几个例子用来帮助理解:
Promise.reject(0).then(result => { // 1
console.log(`成功:${result}`);
return 1;
}).then(result => { // 2
console.log(`成功:${result}`);
return 2;
}).then(result => { // 3
console.log(`成功:${result}`);
return 3;
}, reason => { // 4
console.log(`失败:${reason}`); // 失败:0
});
/**
* try/catch:
* Promise.reject(0):发生异常,1与2只有一个参数,所以捕获不到异常,异常4中被处理
* js:
* Promise.reject(0):返回rejected状态的Promise对象,1与2只有一个参数所以没有执行,则4执行
*/
Promise.resolve(100).then(result => { // 1
console.log(`成功:${result}`); // 成功:100
throw 'xxx';
}).then(result => { // 2
console.log(`成功:${result}`);
return 2;
}).then(result => { // 3
console.log(`成功:${result}`);
return 3;
}, reason => { // 4
console.log(`失败:${reason}`); // 失败:xxx
});
/**
* try/catch:
* Promise.resolve(100):1在执行时抛出异常,被4捕获到
* js:
* Promise.resolve(100):返回fulfiled状态的Promise对象,1执行报错,返回新实例状态为rejected,则4执行
*/
Promise.resolve(100).then(result => { // 1
console.log(`成功:${result}`); // 成功:100
return 1;
}).then(result => { // 2
console.log(`成功:${result}`); // 成功:1
return Promise.reject('NO');
}).catch(reason => { // 3
console.log(`失败:${reason}`); // 失败:NO
});
/**
* Promise.resolve(100):12正常执行,2返回rejected状态的Promise对象,故2返回的新实例状态为rejected,则3执行
*/
1.3 Promise.prototype.catch
Promise.prototype.catch()方法是.then(null, rejection)或.then(undefined, rejection)的别名,用于指定发生错误时的回调函数。
1.4 Promise.prototype.finally
finally()方法用于指定不管Promise对象最后状态如何,都会执行的操作。
finally本质上是then方法的特例:
promise
.finally(() => {
// 语句
});
// 等同于
promise
.then(
result => {
// 语句
return result;
},
error => {
// 语句
throw error;
}
);
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 })
);
};
从上面的实现还可以看到,finally方法总是会返回原来的值。
1.5 Promise.all
Promise.all()方法用于将多个Promise实例,包装成一个新的Promise实例。
const p = Promise.all([p1, p2, p3]);
p的状态由p1、p2、p3决定,分成两种情况。
- 只有
p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。 - 只要
p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。 - 如果作为参数的
Promise实例,自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()的catch方法。
let p1 = Promise.resolve(1);
let p2 = Promise.reject('xxx').catch(err => err);
let p3 = Promise.resolve(3);
const p = Promise.all([p1, p2, p3]);
p.then(res => {
console.log(res); // [1, "xxx", 3]
}).catch(err => {
console.log(err);
})
1.6 Promise.race
Promise.race()方法同样是将多个Promise实例,包装成一个新的Promise实例。
const p = Promise.race([p1, p2, p3]);
只要p1、p2、p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的Promise实例的返回值,就传递给p的回调函数。
let p1 = function () {
return new Promise((resolve, reject) => {
setTimeout(reject, 100, 1);
})
}();
let p2 = function () {
return new Promise((resolve, reject) => {
setTimeout(resolve, 200, 2);
})
}();
let p3 = function () {
return new Promise((resolve, reject) => {
setTimeout(resolve, 300, 3);
})
}();
const p = Promise.race([p1, p2, p3]);
p.then(res => {
console.log(res); // 1
}).catch(err => {
console.log(err);
})
1.7 Promise.allSettled
Promise.allSettled()方法接受一组Promise实例作为参数,包装成一个新的Promise实例。只有等到所有这些参数实例都返回结果,不管是fulfilled还是rejected,包装实例才会结束。
该方法返回的新的Promise实例,一旦结束,状态总是fulfilled,不会变成rejected。状态变成fulfilled后,Promise的监听函数接收到的参数是一个数组,数组中的每个成员对应一个传入Promise.allSettled()的Promise实例。
const p1 = function () {
return new Promise((resolve, reject) => {
setTimeout(reject, 100, 1);
})
}();
const p2 = function () {
return new Promise((resolve, reject) => {
setTimeout(resolve, 200, 2);
})
}();
const p = Promise.allSettled([p1, p2]);
p.then(res => {
console.log(res); // [ {status: "rejected", reason: 1}, {status: "fulfilled", value: 2}]
}).catch(err => {
console.log(err);
})
注意res的格式,每个对象都有status属性,status: 'fulfilled' | 'rejected'。fulfilled时,对象有value属性,rejected时有reason属性,对应两种状态的返回值。
2. Symbol / Iterator / Generator
2.1 Symbol
ES5的对象属性名都是字符串,这容易造成属性名的冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。这就是ES6引入Symbol的原因。
ES6引入了一种新的原始数据类型Symbol,表示独一无二的值。它是JavaScript语言的第七种数据类型,前六种是:undefined、null、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。
Symbol值通过Symbol函数生成。Symbol函数前不能使用new命令,否则会报错。 这是因为生成的Symbol是一个原始类型的值,不是对象。也就是说,由于Symbol值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。
Symbol函数的参数只是表示对当前Symbol值的描述,因此相同参数的Symbol函数的返回值是不相等的。
// 没有参数的情况
let s1 = Symbol();
let s2 = Symbol();
s1 === s2 // false
// 有参数的情况
let s1 = Symbol('foo');
let s2 = Symbol('foo');
s1 === s2 // false
Symbol值不能与其他类型的值进行运算,Symbol只可隐式转换为布尔值,显式转为字符串。否则会报错。
let sym = Symbol('My symbol');
"your symbol is " + sym
// TypeError: can't convert symbol to string
`your symbol is ${sym}`
// TypeError: can't convert symbol to string
String(sym) // 'Symbol(My symbol)'
sym.toString() // 'Symbol(My symbol)'
let sym = Symbol();
Boolean(sym) // true
!sym // false
if (sym) {
// ...
}
Number(sym) // TypeError
sym + 2 // TypeError
Symbol.prototype.description
Symbol.prototype.description用于返回创建Symbol值时的描述。
const sym = Symbol('foo');
sym.description // "foo"
Symbol.for(),Symbol.keyFor()
Symbol.for():它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的Symbol值。如果有,就返回这个Symbol值,否则就新建一个以该字符串为名称的Symbol值,并将其注册到全局(全局环境,不管有没有在全局环境运行)。Symbol.keyFor():返回一个已登记的Symbol类型值的key。
Symbol.for("bar") === Symbol.for("bar") // true
Symbol.for("bar") === Symbol("bar") // false
Symbol("bar") === Symbol("bar") // false
Symbol.iterator
对象的Symbol.iterator属性,指向该对象的默认遍历器方法。
2.2 Iterator
遍历器(Iterator)是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署Iterator接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
Iterator的遍历过程是这样的。
- 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。
- 第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。
- 第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。
- 不断调用指针对象的next方法,直到它指向数据结构的结束位置。
每一次调用
next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含value和done两个属性的对象。其中value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。
下面是一个模拟next方法返回值的例子。
var it = makeIterator(['a', 'b']);
it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }
function makeIterator(array) {
var nextIndex = 0;
return {
next: function() {
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{value: undefined, done: true};
}
};
}
Iterator接口的目的,就是为所有数据结构,提供了一种统一的访问机制,即for...of循环(详见下文)。当使用for...of循环遍历某种数据结构时,该循环会自动去寻找Iterator接口。
默认的Iterator接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。
原生具备Iterator接口的数据结构如下:
- Array
- Map
- Set
- String
- TypedArray
- 函数的arguments对象
- NodeList对象
默认调用Iterator接口的场合:
- 解构赋值
- 扩展运算符
- yield*
- for...of
- Array.from()
- Map(), Set(), WeakMap(), WeakSet()(比如new Map([['a',1],['b',2]]))
- Promise.all()
- Promise.race()
2.3 Generator
执行Generator函数会返回一个遍历器对象,也就是说,Generator函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历Generator函数内部的每一个状态。
Generator函数的执行过程
如下示例:
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 }
上面代码一共调用了四次next方法。
- 第一次调用,
Generator函数开始执行,直到遇到第一个yield表达式为止。next方法返回一个对象,它的value属性就是当前yield表达式的值hello,done属性的值false,表示遍历还没有结束。 - 第二次调用,
Generator函数从上次yield表达式停下的地方,一直执行到下一个yield表达式。next方法返回的对象的value属性就是当前yield表达式的值world,done属性的值false,表示遍历还没有结束。 - 第三次调用,
Generator函数从上次yield表达式停下的地方,一直执行到return语句(如果没有return语句,就执行到函数结束)。next方法返回的对象的value属性,就是紧跟在return语句后面的表达式的值(如果没有return语句,则value属性的值为undefined),done属性的值true,表示遍历已经结束。 - 第四次调用,此时
Generator函数已经运行完毕,next方法返回对象的value属性为undefined,done属性为true。以后再调用next方法,返回的都是这个值。