javascript面试系列(二)——Promise与async/await

在前端面试的技术环节中,异步编程绝对是绕不开的核心考点,而Promise与async/await作为现代JavaScript异步编程的基石,更是面试官重点考察的内容。从"回调地狱"的痛点出发,到Promise的规范落地,再到async/await的语法优化,这三者的演进脉络不仅体现了JavaScript的发展方向,更藏着前端工程师必备的工程化思维。

一、为什么需要Promise?------ 从"回调地狱"说起

在Promise出现之前,JavaScript的异步操作全靠回调函数实现。比如我们需要依次执行"请求用户信息→根据用户ID请求订单→根据订单ID请求商品详情"这一系列异步操作,代码会变成这样:

javascript 复制代码
// 回调地狱示例
requestUserInfo(userId, function(user) {
  requestOrder(user.orderId, function(order) {
    requestGoods(order.goodsId, function(goods) {
      // 业务逻辑
      console.log(goods);
    }, function(err) {
      console.error('请求商品失败', err);
    });
  }, function(err) {
    console.error('请求订单失败', err);
  });
}, function(err) {
  console.error('请求用户失败', err);
});

这种嵌套层级不断加深的代码被称为"回调地狱",它存在三个致命问题:可读性差 (代码呈"金字塔"结构,难以快速梳理执行流程)、可维护性低 (修改中间某一步逻辑需要逐层定位)、错误处理混乱(每个回调都要单独处理错误,无法统一捕获)。

为了解决这些问题,ES6正式引入了Promise规范,它通过"状态管理"的方式将异步操作的结果与后续逻辑解耦,让异步代码变得线性、清晰。

二、Promise核心解析:状态、方法与原理

Promise的本质是一个异步操作的状态管理器,它用标准化的API封装异步操作,让开发者可以专注于业务逻辑而非异步流程控制。要掌握Promise,首先要吃透它的"状态机制"。

1. 三个核心状态与状态不可逆性

Promise有且只有三个状态,且状态一旦改变就会永久固定,无法再修改:

  • pending(等待中) :初始状态,异步操作尚未完成。此时Promise既不成功也不失败。
  • fulfilled(已成功) :异步操作完成且结果有效。状态从pending转为fulfilled后,会触发后续的then回调。
  • rejected(已失败) :异步操作失败或结果无效。状态从pending转为rejected后,会触发后续的catch回调。

面试高频考点:Promise状态不可逆的特性。比如"状态从fulfilled转为rejected是否可行?"答案是不行,一旦状态确定就无法变更。曾有面试题用setTimeout模拟异步,考察对状态固定的理解,务必注意。

2. 基本用法:创建与链式调用

Promise通过构造函数创建,接收一个 executor 函数作为参数,该函数有两个内置参数resolve和reject,分别用于将状态转为fulfilled和rejected:

js 复制代码
// Promise基本用法
const promise = new Promise((resolve, reject) => {
  // 模拟异步操作(比如接口请求)
  setTimeout(() => {
    const success = true;
    if (success) {
      // 成功:传递结果给resolve
      resolve('异步操作成功的结果');
    } else {
      // 失败:传递错误信息给reject
      reject(new Error('异步操作失败'));
    }
  }, 1000);
});

// 链式调用处理结果
promise
  .then(result => {
    console.log('成功接收结果:', result);
    return result + ',已处理'; // 传递给下一个then
  })
  .then(processedResult => {
    console.log('处理后的结果:', processedResult);
  })
  .catch(error => {
    console.error('捕获错误:', error);
  });

这里有两个关键知识点:

  1. then方法的返回值:then方法会返回一个新的Promise对象,这是链式调用的核心。如果then中返回一个非Promise值,会自动包装成fulfilled状态的Promise;如果返回一个Promise,则后续then会等待该Promise状态变更。
  2. catch的作用:catch用于捕获前面所有链式调用中的错误,包括Promise构造函数中reject的错误和then回调中抛出的异常,实现"统一错误处理"。

3. 常用静态方法:all、race、allSettled

除了实例方法,Promise的静态方法在实际开发和面试中都高频出现,重点掌握以下三个:

(1)Promise.all(iterable)

接收一个可迭代对象(如数组),里面包含多个Promise。它的核心特性是"全部成功才成功,一个失败则整体失败":

js 复制代码
const promise1 = Promise.resolve(1);
const promise2 = Promise.resolve(2);
const promise3 = Promise.resolve(3);

Promise.all([promise1, promise2, promise3])
  .then(results => {
    console.log(results); // [1, 2, 3](按传入顺序返回结果)
  })
  .catch(error => {
    console.error(error); // 若任意一个Promise reject,立即触发catch
  });

面试考点:如果Promise.all中某个Promise失败,其他Promise还会继续执行吗?答案是会的。因为Promise状态一旦触发就无法中断,all只是"监听"状态,不会干预单个Promise的执行。

(2)Promise.race(iterable)

同样接收可迭代对象,核心特性是"谁先完成谁生效"------只要有一个Promise状态变更(成功或失败),就立即返回该结果,忽略其他Promise的后续状态:

js 复制代码
// 模拟接口请求超时控制
const request = new Promise((resolve) => {
  setTimeout(() => resolve('请求成功'), 3000);
});
const timeout = new Promise((_, reject) => {
  setTimeout(() => reject(new Error('请求超时')), 2000);
});

Promise.race([request, timeout])
  .then(result => console.log(result))
  .catch(error => console.error(error)); // 2秒后输出"请求超时"

实际用途:常见于接口请求超时控制、资源加载竞争等场景。

(3)Promise.allSettled(iterable)

ES2020新增方法,与all的"一错全错"不同,它会等待所有Promise都完成(无论成功或失败) ,然后返回一个包含所有结果的数组,每个元素包含对应Promise的状态和结果:

js 复制代码
const promise1 = Promise.resolve(1);
const promise2 = Promise.reject(new Error('失败'));

Promise.allSettled([promise1, promise2])
  .then(results => {
    console.log(results);
    // 输出:
    // [
    //   { status: 'fulfilled', value: 1 },
    //   { status: 'rejected', reason: Error: 失败 }
    // ]
  });

适用场景:需要获取所有异步操作的完整结果,比如批量请求数据时,即使部分失败也需要知道失败原因。

三、async/await:Promise的"语法糖"与最佳实践

虽然Promise解决了回调地狱,但链式调用过多时,代码依然会存在"then链"的冗余。ES2017引入的async/await,通过"同步语法写异步逻辑"的方式,进一步简化了Promise的使用,成为当前异步编程的首选方案。

1. 核心语法:async函数与await表达式

async/await的使用依赖两个关键字,规则非常明确:

  1. async关键字:用于声明一个异步函数,该函数的返回值会自动包装成一个Promise对象。
js 复制代码
async function fn() {
    return 1; // 等价于 return Promise.resolve(1)
}
fn().then(result => console.log(result)); // 1
  1. await关键字:只能在async函数内部使用,用于"等待"一个Promise对象的状态变更。它会暂停当前async函数的执行,直到Promise状态变为fulfilled或rejected,然后继续执行函数并返回结果(或抛出错误)。
js 复制代码
// 用async/await重构Promise链式调用
async function getGoodsInfo(userId) {
    try {
     // 等待用户信息请求完成
        const user = await requestUserInfo(userId);
        // 等待订单请求完成(依赖用户信息)
        const order = await requestOrder(user.orderId);
        // 等待商品请求完成(依赖订单信息)
        const goods = await requestGoods(order.goodsId);
        return goods;
     } catch (error) {
         // 统一捕获所有await的错误
         console.error('请求失败:', error);
         throw error; // 可向上层抛出错误
     }
}

 // 调用异步函数
getGoodsInfo(123).then(goods => console.log(goods));

对比之前的回调和Promise链式调用,async/await的优势显而易见:代码完全线性化,逻辑清晰如同步代码,错误处理也通过try/catch统一管理,极大提升了可读性和可维护性。

2. 关键注意点:await的"等待"与并行优化

async/await虽然好用,但如果使用不当,很容易出现"性能问题",这也是面试的高频坑点。比如下面的代码:

js 复制代码
// 错误示例:不必要的串行等待
async function getMultiData() {
  // 两个请求无依赖关系,却串行执行,耗时 = 1s + 1s = 2s
  const data1 = await requestData1();
  const data2 = await requestData2();
  return { data1, data2 };
}

问题在于:requestData1和requestData2之间没有依赖关系,却因为await的串行等待导致总耗时增加。优化方案是先同时触发两个异步操作,再用await等待结果,借助Promise.all实现并行:

js 复制代码
// 优化:并行执行无依赖的异步操作
async function getMultiData() {
  // 同时触发两个请求,无等待
  const promise1 = requestData1();
  const promise2 = requestData2();
  // 等待两个请求都完成,耗时 = 1s(取最长时间)
  const data1 = await promise1;
  const data2 = await promise2;
  return { data1, data2 };
  // 或直接用Promise.all:
  // const [data1, data2] = await Promise.all([promise1, promise2]);
}

面试必考点:async/await的并行优化。核心原则是"无依赖的异步操作尽量并行执行",通过先创建Promise实例(触发异步操作),再await的方式,或直接结合Promise.all,避免不必要的串行等待。

3. 错误处理:try/catch与Promise.catch的结合

await后面的Promise如果变为rejected状态,会抛出一个异常,因此需要用try/catch捕获。但在实际开发中,也可以结合Promise的catch方法,实现更灵活的错误处理:

js 复制代码
// 方式1:局部try/catch(捕获指定await的错误)
async function fn1() {
  try {
    const data = await requestData();
  } catch (error) {
    console.error('requestData失败:', error);
  }
  // 其他逻辑不受影响
  console.log('继续执行');
}

// 方式2:全局catch(捕获async函数的所有错误)
async function fn2() {
  const data = await requestData();
  return data;
}
fn2().catch(error => console.error('fn2执行失败:', error));

实际开发中,可根据错误处理的粒度需求选择合适的方式:局部错误用try/catch,全局或跨函数的错误用catch方法。

四、面试高频题:从原理到实战

掌握了基础后,我们结合面试真题,强化对核心知识点的理解。

真题1:Promise的状态变更与then的执行时机

题目:以下代码的输出顺序是什么?

js 复制代码
console.log('start');
const promise = new Promise((resolve) => {
  console.log('promise executor');
  resolve('success');
});
promise.then(result => {
  console.log(result);
});
console.log('end');

答案:start → promise executor → end → success

解析:Promise构造函数中的executor是同步执行 的,因此会先打印"promise executor";而then方法中的回调是微任务,会在同步代码执行完成后、事件循环的微任务阶段执行,因此"success"会在"end"之后打印。

真题2:async/await与Promise的关系

题目:async/await是Promise的替代方案吗?为什么?

答案:不是替代方案,而是语法糖。原因:

  • async函数的返回值本质是Promise对象,await只能等待Promise对象(如果不是,会自动包装成Promise)。
  • async/await的底层实现依赖Promise和生成器函数(Generator),它并没有脱离Promise的规范,而是简化了Promise的调用方式。
  • 在需要并行执行异步操作、或使用all/race等静态方法时,依然需要结合Promise的API。

真题3:手写Promise简化版(核心逻辑)

题目:实现一个简化版Promise,包含基本的状态管理、resolve/reject和then方法。

js 复制代码
class MyPromise {
  constructor(executor) {
    // 初始状态
    this.status = 'pending';
    // 成功结果
    this.value = undefined;
    // 失败原因
    this.reason = undefined;
    // 成功回调队列(支持多个then)
    this.onFulfilledCallbacks = [];
    // 失败回调队列
    this.onRejectedCallbacks = [];

    // resolve方法:状态转为成功
    const resolve = (value) => {
      // 状态不可逆,只有pending才能修改
      if (this.status === 'pending') {
        this.status = 'fulfilled';
        this.value = value;
        // 执行所有成功回调
        this.onFulfilledCallbacks.forEach(cb => cb(value));
      }
    };

    // reject方法:状态转为失败
    const reject = (reason) => {
      if (this.status === 'pending') {
        this.status = 'rejected';
        this.reason = reason;
        // 执行所有失败回调
        this.onRejectedCallbacks.forEach(cb => cb(reason));
      }
    };

    // 执行executor,捕获同步错误
    try {
      executor(resolve, reject);
    } catch (error) {
      reject(error);
    }
  }

  // then方法:返回新Promise,支持链式调用
  then(onFulfilled, onRejected) {
    // 兼容then未传回调的情况
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v => v;
    onRejected = typeof onRejected === 'function' ? onRejected : e => { throw e };

    const newPromise = new MyPromise((resolve, reject) => {
      // 状态已成功:直接执行回调
      if (this.status === 'fulfilled') {
        setTimeout(() => { // 模拟微任务异步执行
          try {
            const result = onFulfilled(this.value);
            // 回调结果传递给新Promise的resolve
            resolve(result);
          } catch (error) {
            reject(error);
          }
        }, 0);
      }

      // 状态已失败:直接执行回调
      if (this.status === 'rejected') {
        setTimeout(() => {
          try {
            const result = onRejected(this.reason);
            resolve(result);
          } catch (error) {
            reject(error);
          }
        }, 0);
      }

      // 状态 pending:将回调存入队列
      if (this.status === 'pending') {
        this.onFulfilledCallbacks.push(() => {
          setTimeout(() => {
            try {
              const result = onFulfilled(this.value);
              resolve(result);
            } catch (error) {
              reject(error);
            }
          }, 0);
        });

        this.onRejectedCallbacks.push(() => {
          setTimeout(() => {
            try {
              const result = onRejected(this.reason);
              resolve(result);
            } catch (error) {
              reject(error);
            }
          }, 0);
        });
      }
    });

    return newPromise;
  }
}

解析:简化版Promise的核心是实现"状态不可逆"和"then链式调用"。关键点包括:用队列存储pending状态时的回调、then返回新Promise以支持链式调用、通过setTimeout模拟微任务异步执行、捕获回调中的错误并传递给下一个Promise的reject。

五、总结:从入门到面试通关的核心要点

Promise与async/await的核心是"解决异步编程的流程控制问题",面试考察的不仅是API用法,更是对底层原理和工程实践的理解。最后梳理核心考点:

  1. Promise的三个状态及不可逆性,executor同步执行、then回调异步执行的特性。
  2. then的链式调用原理(返回新Promise)和catch的错误捕获范围。
  3. all/race/allSettled的区别及适用场景,尤其是all的"一错全错"和race的"先到先得"。
  4. async/await的语法规则,与Promise的依赖关系,以及并行优化的技巧。
  5. 异步代码的执行顺序(同步→微任务→宏任务),结合Promise和async/await的实际场景分析。
相关推荐
2022.11.7始学前端3 小时前
n8n第七节 只提醒重要的待办
前端·javascript·ui·n8n
SakuraOnTheWay3 小时前
React Grab实践 | 记一次与Cursor的有趣对话
前端·cursor
阿星AI工作室4 小时前
gemini3手势互动圣诞树保姆级教程来了!附提示词
前端·人工智能
徐小夕4 小时前
知识库创业复盘:从闭源到开源,这3个教训价值百万
前端·javascript·github
xhxxx4 小时前
函数执行完就销毁?那闭包里的变量凭什么活下来!—— 深入 JS 内存模型
前端·javascript·ecmascript 6
StarkCoder4 小时前
求求你试试 DiffableDataSource!别再手算 indexPath 了(否则迟早崩)
前端
fxshy4 小时前
Cursor 前端Global Cursor Rules
前端·cursor
红彤彤4 小时前
前端接入sse(EventSource)(@fortaine/fetch-event-source)
前端
L、2184 小时前
统一日志与埋点系统:在 Flutter + OpenHarmony 混合架构中实现全链路可观测性
javascript·华为·智能手机·electron·harmonyos
WindStormrage4 小时前
umi3 → umi4 升级:踩坑与解决方案
前端·react.js·cursor