关注微信公众号:前端充电宝,获取最新原创文章!
目录
下面题目主要和Promise以及JS的事件循环有关,题目均为代码输出问题(面试题从文章第二部分开始)。
Promise回顾
首先来回顾一下Promise是什么。
Promise 是异步编程的一种解决方案,比传统的解决方案回调函数和事件更合理和更强大。
所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。
(1)Promise的实例有三个状态:
- Pending(进行中)
- Resolved(已完成)
- Rejected(已拒绝)
当把一件事情交给promise时,它的状态就是Pending,任务完成了状态就变成了Resolved、没有完成失败了就变成了Rejected。
(2)Promise的实例有两个过程:
- pending -> fulfilled : Resolved(已完成)
- pending -> rejected:Rejected(已拒绝)
需要注意:一旦从进行状态变成为其他状态就永远不能更改状态了。
特点:
- 将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。流程更加清晰,代码更加优雅。
- Promise对象提供统一的接口,使得控制异步操作更加容易。
缺点:
- 无法取消Promise,一旦新建它就会立即执行,无法中途取消。
- 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
- 当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
实例:
当我们在构造 Promise 的时候,构造函数内部的代码是立即执行的:
newPromise((resolve, reject)=>{
console.log('new Promise')resolve('success')})
console.log('finifsh')// new Promise -> finifsh
Promise 实现了链式调用,也就是说每次调用 then 之后返回的都是一个 Promise,并且是一个全新的 Promise,原因也是因为状态不可变。如果你在 then 中 使用了 return,那么 return 的值会被 Promise.resolve() 包装
Promise.resolve(1).then(res=>{
console.log(res)// => 1return2// 包装成 Promise.resolve(2)}).then(res=>{
console.log(res)// => 2})
当然了,Promise 也很好地解决了回调地狱的问题,可以把之前的回调地狱例子改写为如下代码:
ajax(url).then(res=>{
console.log(res)returnajax(url1)}).then(res=>{
console.log(res)returnajax(url2)}).then(res=> console.log(res))
前面都是在讲述 Promise 的一些优点和特点,其实它也是存在一些缺点的,比如无法取消 Promise,错误需要通过回调函数捕获。
Promise基础
1、下面代码的执行结果是
const promise=newPromise((resolve, reject)=>{
console.log(1);
console.log(2);});
promise.then(()=>{
console.log(3);});
console.log(4);
最后应该输出1 2 4。这样最主要的就是3,要知道promise.then是微任务,会在所有的宏任务执行完之后才会执行,同时需要promise内部的状态发生变化,因为这里内部没有发生变化,所以不输出3。
2、下面代码的执行结果是
const promise1=newPromise((resolve, reject)=>{
console.log('promise1')resolve('resolve1')})const promise2= promise1.then(res=>{
console.log(res)})
console.log('1', promise1);
console.log('2', promise2);
输出结果如下:
'promise1''1' Promise{<resolved>:'resolve1'}'2' Promise{<pending>}'resolve1'
需要注意的是,直接打印promise1,会打印出它的状态值和参数。
这里说一下这道题的具体思路:
- script是一个宏任务,按照顺序执行这些代码
- 首先进入Promise,执行该构造函数中的代码,打印
promise1
- 碰到
resolve
函数, 将promise1
的状态改变为resolved
, 并将结果保存下来 - 碰到
promise1.then
这个微任务,将它放入微任务队列 promise2
是一个新的状态为pending
的Promise
- 执行同步代码1, 同时打印出
promise1
的状态是resolved
- 执行同步代码2,同时打印出
promise2
的状态是pending
- 宏任务执行完毕,查找微任务队列,发现
promise1.then
这个微任务且状态为resolved
,执行它。
这样,就执行完了所有的的代码。
3、下面代码的执行结果是
const promise=newPromise((resolve, reject)=>{
console.log(1);setTimeout(()=>{
console.log("timerStart");resolve("success");
console.log("timerEnd");},0);
console.log(2);});
promise.then((res)=>{
console.log(res);});
console.log(4);
- 首先遇到Promise构造函数,会先执行里面的内容,打印1
- 遇到
steTimeout
,它是一个宏任务,被推入宏任务队列 - 接下继续执行,打印出2
- 由于
Promise
的状态此时还是pending
,所以promise.then
先不执行 - 继续执行下面的同步任务,打印出4
- 微任务队列此时没有任务,继续执行下一轮宏任务,执行
steTimeout
- 首先执行
timerStart
,然后遇到了resolve
,将promise
的状态改为resolved
且保存结果并将之前的promise.then
推入微任务队列,再执行timerEnd
- 执行完这个宏任务,就去执行微任务
promise.then
,打印出resolve
的结果
4、 下面代码的执行结果是
Promise.resolve().then(()=>{
console.log('promise1');const timer2=setTimeout(()=>{
console.log('timer2')},0)});const timer1=setTimeout(()=>{
console.log('timer1')
Promise.resolve().then(()=>{
console.log('promise2')})},0)
console.log('start');
这个题目就有点绕了,下面来梳理一下:
- 首先,
Promise.resolve().then
是一个微任务,加入微任务队列 - 执行timer1,它是一个宏任务,加入宏任务队列
- 继续执行下面的同步代码,打印出start
- 这样第一轮的宏任务就执行完了,开始执行微任务,打印出
promise1
- 遇到timer2,它是一个宏任务,将其加入宏任务队列
- 这样第一轮的微任务就执行完了,开始执行第二轮宏任务,指执行定时器timer1,打印timer1
- 遇到Promise,它是一个微任务,加入微任务队列
- 开始执行微任务队列中的任务,打印promise2
- 最后执行宏任务timer2定时器,打印出timer2
这个题目还是比较复杂的,值得去认真理解一下。
执行结果:
'start''promise1''timer1''promise2''timer2'
5、 下面代码的执行结果是
const promise=newPromise((resolve, reject)=>{resolve('success1');reject('error');resolve('success2');});
promise.then((res)=>{
console.log('then:', res);}).catch((err)=>{
console.log('catch:', err);})
执行结果为:then:success1
这个题目考察的就是Promise
的状态在发生变化之后,就不会再发生变化。开始状态由pending
变为resolve
,说明已经变为已完成状态,下面的两个状态的就不会再执行,同时下面的catch也不会捕获到错误。
6、下面代码的执行结果是
Promise.resolve(1).then(2).then(Promise.resolve(3)).then(console.log)
执行结果为:
1
Promise{<fulfilled>:undefined}
Promise.resolve方法的参数如果是一个原始值,或者是一个不具有then方法的对象,则Promise.resolve方法返回一个新的Promise对象,状态为resolved,Promise.resolve方法的参数,会同时传给回调函数。
then方法接受的参数是函数,而如果传递的并非是一个函数,它实际上会将其解释为then(null),这就会导致前一个Promise的结果会传递下面。
7、下面代码的执行结果是
const promise1=newPromise((resolve, reject)=>{setTimeout(()=>{resolve('success')},1000)})const promise2= promise1.then(()=>{thrownewError('error!!!')})
console.log('promise1', promise1)
console.log('promise2', promise2)setTimeout(()=>{
console.log('promise1', promise1)
console.log('promise2', promise2)},2000)
输出的结果如下:
promise1 Promise{<pending>}
promise2 Promise{<pending>}Uncaught(in promise) Error: error!!!
promise1 Promise{<fulfilled>:"success"}
promise2 Promise{<rejected>: Error: error!!}
这个就比较好理解了,和上面的几个题目思路类似。
Promise的catch、then、finally
8、下面代码的执行结果是
Promise.resolve(1).then(res=>{
console.log(res);return2;}).catch(err=>{return3;}).then(res=>{
console.log(res);});
输出结果为:1 2
Promise可以链式调用,因为每次调用.then
或者.catch
都会返回一个新的 promise,从而实现了链式调用, 它并不像一般我们任务的链式调用一样return this。
上面的输出结果之所以依次打印出1和2,是因为resolve(1)
之后走的是第一个then方法,并没有走catch里,所以第二个then中的res得到的实际上是第一个then的返回值。并且return 2会被包装成resolve(2)
,被最后的then打印输出2。
9、下面代码的执行结果是
Promise.resolve().then(()=>{returnnewError('error!!!')}).then(res=>{
console.log("then: ", res)}).catch(err=>{
console.log("catch: ", err)})
返回任意一个非 promise 的值都会被包裹成 promise 对象,因此这里的return new Error('error!!!')
也被包裹成了return Promise.resolve(new Error('error!!!'))
。
因此它被then捕获而不是catch,输出结果为:
"then: ""Error: error!!!"
10、下面代码的执行结果是
const promise= Promise.resolve().then(()=>{return promise;})
promise.catch(console.err)
这里其实是一个坑,.then
或.catch
返回的值不能是 promise 本身,否则会造成死循环。所以这道题会报错:
Uncaught(in promise) TypeError: Chaining cycle detectedfor promise #<Promise>
11、下面代码的执行结果是
Promise.resolve(1).then(2).then(Promise.resolve(3)).then(console.log)
看到这个题目,好多的then,实际上我们只需要记住一个原则:.then
或.catch
的参数期望是函数,传入非函数则会发生值透传。
第一个then和第二个then中传入的都不是函数,一个是数字,一个是对象,因此发生了透传,将resolve(1)
的值直接传到最后一个then里。
所以输出结果为:1
12、下面代码的执行结果是
Promise.reject('err!!!').then((res)=>{
console.log('success', res)},(err)=>{
console.log('error', err)}).catch(err=>{
console.log('catch', err)})
.then
函数中的两个参数:
- 第一个参数是用来处理Promise成功的函数
- 第二个则是处理失败的函数
也就是说Promise.resolve('1')
的值会进入成功的函数,Promise.reject('2')
的值会进入失败的函数。
在这道题中,错误直接被then
的第二个参数捕获了,所以就不会被catch
捕获了,输出结果为:'error' 'error!!!'
但是,如果是像下面这样:
Promise.resolve().then(functionsuccess(res){thrownewError('error!!!')},functionfail1(err){
console.log('fail1', err)}).catch(functionfail2(err){
console.log('fail2', err)})
在then
的第一参数中抛出了错误,那么他就不会被第二个参数不活了,而是被后面的catch
捕获到,所以输出结果为:
fail2 Error: error!!!
at success
13、下面代码的执行结果是
Promise.resolve('1').then(res=>{
console.log(res)}).finally(()=>{
console.log('finally')})
Promise.resolve('2').finally(()=>{
console.log('finally2')return'我是finally2返回的值'}).then(res=>{
console.log('finally2后面的then函数', res)})
.finally()
一般用的很少,只要记住以下几点就可以了:
.finally()
方法不管Promise对象最后的状态如何都会执行.finally()
方法的回调函数不接受任何的参数,也就是说你在.finally()
函数中是无法知道Promise最终的状态是resolved
还是rejected
的- 它最终返回的默认会是一个上一次的Promise对象值,不过如果抛出的是一个异常则返回异常的Promise对象。
- finally本质上是then方法的特例
上面的代码的输出结果为:
1
finally2
finally
finally2后面的then函数2
再开看一下.finally()
的错误捕获:
Promise.resolve('1').finally(()=>{
console.log('finally1')thrownewError('我是finally中抛出的异常')}).then(res=>{
console.log('finally后面的then函数', res)}).catch(err=>{
console.log('捕获错误', err)})
输出结果为:
'finally1''捕获错误' Error: 我是finally中抛出的异常
Promise的all和race
.all()
的作用是接收一组异步任务,然后并行执行异步任务,并且在所有异步操作执行完后才执行回调。.race()
的作用是接收一组异步任务,然后并行执行异步任务,只保留取第一个执行完成的异步操作的结果,其他的方法仍在执行,不过执行结果会被抛弃。
14、下面代码的执行结果是
functionrunAsync(x){const p=newPromise(r=>setTimeout(()=>r(x, console.log(x)),1000))return p}
Promise.all([runAsync(1),runAsync(2),runAsync(3)]).then(res=> console.log(res))
首先,我们定义了一个Promise,来异步执行函数runAsync,该函数传入一个值x,然后间隔一秒后打印出这个x。
之后再使用Promise.all
来执行这个函数,结果如下:
123[1,2,3]
执行的时候,看到一秒之后输出了1,2,3,同时输出了数组[1, 2, 3],三个函数是同步执行的,并且在一个回调函数中返回了所有的结果。并且结果和函数的执行顺序是一致的。
15、下面代码的执行结果是
functionrunAsync(x){const p=newPromise(r=>setTimeout(()=>r(x, console.log(x)),1000))return p}functionrunReject(x){const p=newPromise((res, rej)=>setTimeout(()=>rej(`Error:${x}`, console.log(x)),1000* x))return p}
Promise.all([runAsync(1),runReject(4),runAsync(3),runReject(2)]).then(res=> console.log(res)).catch(err=> console.log(err))
输出结果:
// 1s后输出13// 2s后输出2
Error:2// 4s后输出4
我们可以看到。catch捕获到了第一个错误,在这道题目中最先的错误就是runReject(2)
的结果。
如果一组异步操作中有一个异常都不会进入.then()
的第一个回调函数参数中。会被.then()
的第二个回调函数捕获。
16、下面代码的执行结果是
下面再来看一下race:
functionrunAsync(x){const p=newPromise(r=>setTimeout(()=>r(x, console.log(x)),1000))return p}
Promise.race