Promise 常见面试题

94 阅读7分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第9天,点击查看活动详情

了解异步

要想了解 Promies 首先肯定要知道, 他的出处, 为什么会有这个东西? 异步缘由?

  • 异步和同步? 要了解异步,首先就要知道 什么是同步, 要了解以下 JavaScript 的工作机制了, 因为 js 是单线程, 所以同一时间 只能处理一个任务, 剩余任务要进行排队,也就是我们面试的时候 也会经常问到的 eventLoop, 前一个任务执行完,才能执行下一个任务 , 同时 还要了解宏任务 和 微任务 这种异步队列的执行时机, 这些知识点在这里我们就不先进行深入的探究了。
    那么在 ES6 的异步解决方案出来之前,我们的 js 想要在未来的某个时间,响应一个事件是如何处理的呢? 想必不用说 大家心理也一定有了答案, 没错,那就是 回调, 而当我们的异步处理 依赖越来越深的时候, 这时候就会出现我们熟知的 回调地狱了, 面试的时候也会经常问我们 Promise 解决了什么问题? 最基本的 也要把回调地狱回答上来, 再进阶一些的 可以回答一下 信任问题, 这一点如何扩散呢?
  • Promise 解决了什么问题? 在没有 Promise 的时候, 我们使用回调函数来处理未来需要执行的某个时间, 实际也就是将我们的回调 交给了 另外一个 处理逻辑, 也可以称之为第三方, 而这部分将来要执行的代码, 可能会出现很多问题, 我们需要思考,如果代码报错怎么办? 如果回调中 处理的某些类型错误 如何捕获? 一个请求,超时了怎么办? 当然,我们也有很多的库 给出了很多的解决方案, 但都是基于我们本身 js 错误捕获的能力 进行的二次封装,如 try catch。
    而 Promise 解决的就是回调托管的一个信任问题,我们如何信任这个 API 呢? 这要同他的一个 封装思维有关了,也就是 分离式回调设计
  • 分离式回调设计 异步的形式在我们 node 中一直都有,不过使用的风格是 error-first , 也就是我们回调中第一个参数,是错误处理, 第二个才是逻辑处理,而 Promise 并没有采用这种方式,而是把错误的回调放在了第二个位置

一些面试题

setTimeout(() => {
  console.log('1');
  Promise.resolve().then(() => {
    console.log('promise')
  })
}, 0)
setTimeout(() => {
  console.log('2')
}, 0)
console.log('start')
  • 这道题 加上了 定时器, 也就是考察了宏任务 和微任务的顺序关系,真实打印是:start 1 promise 2 , 定时器 属于宏任务, start 在我们的脚本中, 会先执行, 第一个定时器 和第二个定时器分别放到 调用栈, 然后执行 第一个定时器, 之后执行 第一个定时器里面的 同步任务, 异步任务, 执行完成以后 再去执行第二个定时器。、
console.log("1")
// setTimeOut函数不设置时间
setTimeout ( () => {
    console.log('2')
    new Promise( (resolve => {
        console.log("3")
        resolve()
    })) .then ( () => console.log("4"))
})
new Promise ( (resolve, reject) => {
    console.log("5")
    resolve()
}) .then( () => console.log('6'))
setTimeout ( () => {
    console.log('7')
})
console.log("8")
  • 这道题和上面那一道 相似, 真实打印是: 15862347 js代码从上到下执行,先遇到console.log(1),将其加入主线程的宏任务队列,然后发现setTimeOut,为其创建第二个宏任务队列并将其加入,其中代码先不执行,然后遇到Promise,将其中的console.log(5)加入主线程的宏任务队列,将then回调函数中的内容加入微任务队列,继续往下发现第二个setTimeOut,将其放入第三个宏任务队列,最后将console.log(8)放入主线程宏任务队列,到此,代码已经完成了在不同队列中的分布,详细情况为: 接下来开始执行第一个宏任务队列,分别打印1,5,8,然后执行微任务队列,打印6,微任务队列变为空。接着执行第二个宏任务队列,开始执行第一个setTimeOut中的代码:先后打印 2,3,并将回调函数中的console.log(4)加入微任务。此时第二个宏任务队列执行完毕,开始执行微任务队列,打印 4。接着执行第三个宏任务队列,打印 7。执行完毕。
Promise.resolve().then(() => {
    console.log(1);
    return Promise.resolve(2);
}).then((res) => {
    console.log(res)
})

Promise.resolve().then(() => {
    console.log(4);
}).then(() => {
    console.log(5);
}).then(() => {
    console.log(6);
}).then(() => {
    console.log(7);
}).then(() =>{
    console.log(8);
})
  • 这道题如果我们平时没有接触过这种场景 或者做过这种题的话, 应该很难猜的出来答案了, 真实打印是: 1456278 , 至于为什么会这么打印,也有一些人在分享 promise 什么 A, B 这个标准那个标准的, 这个实现那个实现, 都没啥用, 这里就是 Promise 在最初设计的时候, 当两个 Promise 连续进行 then 方法的时候, 其中一个执行了 三次以后, 会跳转到另一个微任务, 也就是另一个 Promise 的 then 调用中去, 这个原理我们手写也是无法实现的。
async function async1() {
 console.log('async1 start');
 await async2();
 console.log('asnyc1 end');
}
async function async2() {
 console.log('async2');
}
console.log('script start');
setTimeout(() => {
 console.log('setTimeOut');
}, 0);
async1();
new Promise(function (reslove) {
 console.log('promise1');
 reslove();
}).then(function () {
 console.log('promise2');
})
console.log('script end');
  • 这道相对来讲难度不是很大, 主要是考察 async 和 await 的执行时机, await 的执行不会阻塞外部函数, await 也会对其等待进行包装, 如果不是 Promise, 则使用 Promise.resolve 进行包装,执行顺序如下:
  1. 打印script 宏任务中的 script start
  2. 执行 async1, 打印 async1 start, await 这里有一个机制, 就是 await 的等待, 不会阻塞外部函数的执行, 而 await 等待的 如果是一个 Promise 则 Promise 里面的代码还是同步执行, 如果不是 Promise ,就会使用 Promise.resolve 来进行封装, 这里的 async2 是一个 async 方法, 里面的 打印会同步执行, 而 await async2 后面的代码 会放到微任务队列中的第一个位置,等待外部同步代码执行完毕以后再执行
  3. 执行 async2, 打印 async2
  4. 执行 Promise 打印 promise1 , 同时把 then 放入微任务队列
  5. 打印 script end
  6. 打印 async1 end , 执行微任务1
  7. 微任务 promise2 , 执行微任务2
  8. 定时 宏任务 setTimeOut
setTimeout(()=>{console.log(1);},0)
asyncFn();
const promise = new Promise((res,rej)=>{
  console.log(2);
  rej()
}).then(console.log.bind(null,3),console.log.bind(null,4)).catch(console.log.bind(null,5));
async function asyncFn(){
  const res1 = await 6;
  console.log(res1);
  const res2 = await new Promise((res,rej)=>{
    console.log(7);
  })
  console.log(8);
}
console.log(9);
  • 这道题考察了宏任务、微任务 和 bind 方法 和 async await, 真实打印是 2、 9、 6、 7、 4 undefined、 1, 这里还要了解一些 then 回调, 是接收两个参数, 第一个是 resolve, 第二个是 reject, 如果我们在 then 中 传入了第二个回调,那么后面的 catch 便不会执行了, await 会阻塞当前函数后面的代码, 但不会阻塞当前函数以外的外部代码, 说着这里打印完 2 先打印 9 , 这里 Promise 中的 代码是同步执行, 这一点就不必多说了, 在这个 Promise 之前 执行 asyncFn, 那么就会执行 await, 这个 await 会等待一个 Promise, 如果我们等待的不是一个 Promise, 则会进行包装 使用 Promise.resolve 进行一层包装, 所以 res1 实际上就是 6 , 7 也会执行, 7 这个 Promise 没有返回状态, 也就是 一个 pending 的状态,所以会阻塞后面的执行 8 不会打印, 然后执行 bind , 这里的 会执行 log.bind(null,4), 这里是 修改 log 的 this 指向, 并且传入了一个参数 4, 而 bind 接受参数的分布传入, 上面的 rej 在执行的时候, 没有传入参数, 实际上就是一个 undefined, 也就是传入 then 方法 第二个回调的参数 是undefined。

大家如果有其他有趣的题型 可以评论区 或者发给我 一起讨论哦~