JS 中async/await關鍵詞的使用解析

在 JavaScript 異步編程領域,async/await是 ES2017 推出的革命性語法糖,它基於 Promise 實現,核心作用是用同步代碼的寫法實現異步邏輯,徹底解決了傳統回調函數(回調地獄)和 Promise 鏈式調用(.then()嵌套)的可讀性問題。async用於標記函數為異步函數,await用於暫停異步函數執行、等待 Promise 完成並獲取其結果。掌握async/await是現代 JS 異步開發的必備技能,尤其在處理接口請求、文件讀寫、定時任務等異步場景時,能讓代碼邏輯更清晰、更易維護。

一、async/await 的基礎用法

1. 核心語法規則
  • async:修飾函數(普通函數 / 箭頭函數),使其成為異步函數,異步函數的返回值會自動包裝為 Promise 對象(即使返回基本類型,也會被Promise.resolve()包裹);

  • await:只能在async函數內部使用,作用於 Promise 對象時,會暫停函數執行,直到 Promise 狀態變為fulfilled(成功)或rejected(失敗),並返回 Promise 的結果(成功值 / 拋出錯誤)。

2. 基礎示例:同步寫法實現異步
// 模擬異步接口請求(返回Promise)
function fetchUser() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({ name: "張三", age: 28 });
    }, 1000);
  });
}

// 異步函數:用async標記,內部用await等待Promise
async function getUserInfo() {
  console.log("開始請求用户數據");
  // 暫停執行,等待fetchUser的Promise完成,獲取結果
  const user = await fetchUser();
  console.log("請求完成:", user);
  return user; // 返回值自動包裝為Promise
}

// 調用異步函數(返回Promise,可通過.then()獲取結果)
getUserInfo().then(res => {
  console.log("最終結果:", res);
});

// 輸出順序(間隔1秒):
// 開始請求用户數據
// 請求完成: { name: '張三', age: 28 }
// 最終結果: { name: '張三', age: 28 }
3. 異步函數的返回值特性
// 1. 返回基本類型:自動包裝為Promise
async function returnBasic() {
  return "hello async";
}
console.log(returnBasic()); // 輸出:Promise { 'hello async' }
returnBasic().then(res => console.log(res)); // 輸出:hello async

// 2. 返回Promise:直接返回該Promise(不二次包裝)
async function returnPromise() {
  return new Promise((resolve) => resolve(123));
}
returnPromise().then(res => console.log(res)); // 輸出:123

// 3. 無返回值:默認返回Promise { undefined }
async function noReturn() {}
noReturn().then(res => console.log(res)); // 輸出:undefined

二、await 的核心特性

1. 等待 Promise 成功並獲取結果

await後跟 Promise 對象時,會 “解包” Promise 的成功值,直接返回給變量,無需.then()鏈式調用。

// 模擬多個異步請求
function fetchUser() { return Promise.resolve({ id: 1 }); }
function fetchUserPosts(userId) { 
  return Promise.resolve([{ postId: 101, userId }]); 
}

async function getUserPosts() {
  // 第一步:獲取用户信息(等待Promise完成)
  const user = await fetchUser();
  // 第二步:用用户ID獲取帖子(依賴第一步結果,同步寫法)
  const posts = await fetchUserPosts(user.id);
  console.log("用户帖子:", posts);
  return posts;
}

getUserPosts();
// 輸出:用户帖子: [ { postId: 101, userId: 1 } ]
2. 處理 Promise 失敗(錯誤捕獲)

await後的 Promise 若變為rejected狀態,會直接拋出錯誤,需通過try/catch捕獲(替代 Promise 的.catch()),這是async/await處理錯誤的標準方式。

// 模擬失敗的異步請求
function fetchFailed() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      reject(new Error("請求失敗:網絡異常"));
    }, 1000);
  });
}

async function handleError() {
  try {
    // 等待失敗的Promise,會拋出錯誤
    const result = await fetchFailed();
    console.log("請求成功:", result);
  } catch (error) {
    // 捕獲錯誤並處理
    console.error("捕獲錯誤:", error.message);
  }
}

handleError();
// 輸出:捕獲錯誤: 請求失敗:網絡異常
3. 並行執行多個異步任務

默認情況下,await是串行執行(前一個完成才執行下一個),若多個異步任務無依賴關係,需用Promise.all()實現並行,提升執行效率。

// 模擬兩個獨立的異步請求
function fetchA() { return new Promise(resolve => setTimeout(() => resolve("A"), 1000)); }
function fetchB() { return new Promise(resolve => setTimeout(() => resolve("B"), 1000)); }

// 串行執行(總耗時≈2秒)
async function serialExecute() {
  console.time("serial");
  const a = await fetchA();
  const b = await fetchB();
  console.log("串行結果:", a, b);
  console.timeEnd("serial"); // 輸出:serial: 2000+ms
}

// 並行執行(總耗時≈1秒)
async function parallelExecute() {
  console.time("parallel");
  // 先創建Promise實例(啓動異步任務)
  const promiseA = fetchA();
  const promiseB = fetchB();
  // 同時等待多個Promise完成
  const a = await promiseA;
  const b = await promiseB;
  // 或直接用Promise.all:const [a, b] = await Promise.all([fetchA(), fetchB()]);
  console.log("並行結果:", a, b);
  console.timeEnd("parallel"); // 輸出:parallel: 1000+ms
}

serialExecute();
parallelExecute();

三、async/await 的核心應用場景

1. 處理 AJAX / 接口請求(替代.then ())

這是async/await最常用的場景,結合fetch/axios等請求庫,讓異步請求邏輯更直觀。

// 基於fetch的接口請求(async/await版)
async function fetchApiData(url) {
  try {
    // 等待響應
    const response = await fetch(url);
    // 檢查HTTP狀態碼(fetch僅在網絡錯誤時reject,狀態碼非2xx需手動判斷)
    if (!response.ok) {
      throw new Error(`HTTP錯誤:${response.status}`);
    }
    // 等待JSON解析
    const data = await response.json();
    return data;
  } catch (error) {
    console.error("接口請求失敗:", error.message);
    // 拋出錯誤,讓調用方處理
    throw error;
  }
}

// 調用
fetchApiData("https://api.example.com/user")
  .then(data => console.log("接口數據:", data))
  .catch(err => console.log("最終錯誤處理:", err));
2. 異步遍歷(for...of + await)

forEach循環中無法直接使用await(forEach 是同步遍歷,會忽略 await),需改用for...of循環實現異步遍歷。

// 模擬批量異步請求
function fetchItem(id) {
  return new Promise(resolve => {
    setTimeout(() => resolve(`商品${id}`), 500);
  });
}

// 異步遍歷數組
async function batchFetch(ids) {
  const results = [];
  // for...of支持await,串行遍歷
  for (const id of ids) {
    const item = await fetchItem(id);
    results.push(item);
    console.log("獲取到:", item);
  }
  return results;
}

batchFetch([1, 2, 3]);
// 輸出(間隔500ms):
// 獲取到: 商品1
// 獲取到: 商品2
// 獲取到: 商品3
3. 結合 Promise.race 實現超時控制

Promise.race()可讓多個 Promise 競爭,結合async/await能輕鬆實現異步任務的超時控制。

// 封裝超時函數
function timeout(delay) {
  return new Promise((_, reject) => {
    setTimeout(() => {
      reject(new Error(`請求超時(${delay}ms)`));
    }, delay);
  });
}

// 帶超時控制的異步請求
async function fetchWithTimeout(url, delay = 3000) {
  try {
    // 競爭:請求和超時誰先完成,就執行誰
    const result = await Promise.race([
      fetch(url),
      timeout(delay)
    ]);
    return result.json();
  } catch (error) {
    console.error("請求超時/失敗:", error.message);
    return null;
  }
}

// 調用(若接口3秒內未返回,觸發超時)
fetchWithTimeout("https://api.example.com/slow-data", 3000);
4. 頂層 await(ES2022)

ES2022 允許在模塊頂層使用await(無需包裹在 async 函數中),適用於模塊加載時的異步初始化(如加載配置、預請求數據)。

// 模塊文件(如config.js)
// 頂層await:加載遠程配置
const config = await fetch("https://api.example.com/config").then(res => res.json());
export default config;

// 主文件
import config from "./config.js";
console.log("配置加載完成:", config);

注意:頂層 await 僅在 ES 模塊(type="module"的腳本 /import/export模塊)中生效,普通腳本中仍需包裹在 async 函數中。

四、async/await 的注意事項

1. await 只能在 async 函數中使用

普通函數中使用await會直接拋出語法錯誤,即使是回調函數,也需確保其所在函數是 async 函數。

// 錯誤示例:普通函數中使用await
function normalFunc() {
  const res = await fetchUser(); // 報錯:Uncaught SyntaxError: await is only valid in async functions
}

// 正確示例:回調函數標記為async
setTimeout(async () => {
  const res = await fetchUser();
  console.log(res);
}, 1000);
2. 未捕獲的錯誤會導致 Promise 拒絕

若async函數內部的await拋出錯誤且未用try/catch捕獲,異步函數返回的 Promise 會變為rejected狀態,需在調用時用.catch()捕獲,否則會觸發未捕獲的 Promise 錯誤。

async function uncaughtError() {
  await Promise.reject(new Error("未捕獲的錯誤"));
}

// 錯誤:未捕獲的Promise錯誤
// uncaughtError();

// 正確:調用時捕獲
uncaughtError().catch(err => console.error("捕獲錯誤:", err.message));
3. 避免濫用 await(串行阻塞並行)

若多個異步任務無依賴關係,不要逐個await(串行執行),應使用Promise.all()並行執行,否則會浪費性能。

// 不推薦:串行執行(總耗時≈2秒)
async function badParallel() {
  const a = await fetchA();
  const b = await fetchB();
  return [a, b];
}

// 推薦:並行執行(總耗時≈1秒)
async function goodParallel() {
  const [a, b] = await Promise.all([fetchA(), fetchB()]);
  return [a, b];
}
4. async 函數中的 return 會包裝為 Promise

即使async函數內部用return返回值,調用時也需用.then()或await獲取,直接賦值會得到 Promise 對象。

async function getNum() {
  return 100;
}

const num = getNum();
console.log(num); // 輸出:Promise { 100 }
console.log(await getNum()); // 輸出:100(需在async函數中)

五、async/await vs Promise vs 回調函數

特性 回調函數 Promise async/await
代碼可讀性 差(回調地獄) 中(鏈式調用) 優(同步寫法)
錯誤處理 嵌套 try/catch .catch () 鏈式捕獲 try/catch(同步方式)
並行執行 需手動控制 Promise.all/race 結合 Promise.all/race
調試難度 高(棧混亂) 中(鏈式棧) 低(同步棧)
兼容性 全兼容 ES6+ ES2017+/ 轉譯後兼容

總結

  1. async標記函數為異步函數,其返回值自動包裝為 Promise;await僅在 async 函數內生效,用於暫停執行並獲取 Promise 結果。

  2. async/await的錯誤處理依賴try/catch,未捕獲的錯誤會導致 Promise 拒絕,需在調用時用.catch()兜底。

  3. 無依賴的異步任務需用Promise.all()並行執行,避免串行阻塞;結合Promise.race()可實現超時控制。

  4. async/await是 Promise 的語法糖,完全兼容 Promise 生態,是現代 JS 異步編程的首選方式,大幅提升代碼可讀性和可維護性。