Promise面试题汇总

2022-10-31 14:46:22

关注微信公众号:前端充电宝,获取最新原创文章!

下面题目主要和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是一个新的状态为pendingPromise
  • 执行同步代码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
  • 作者:CUG-GZ
  • 原文链接:https://cuggz.blog.csdn.net/article/details/108129645
    更新时间:2022-10-31 14:46:22