JS 中 yield 關鍵詞的使用解析

在 JavaScript 異步編程和迭代器開發中,yield是一個極具特殊性的關鍵詞。它僅能在生成器函數(帶function*標識的函數)內部使用,核心作用是 “暫停” 生成器函數的執行,並向外返回一個值;當生成器通過next()方法繼續執行時,yield又能接收next()傳入的參數,作為自身的返回值。這種 “暫停 - 恢復” 的能力,讓yield成為處理複雜迭代邏輯、異步流程控制的重要工具,尤其在處理數據流、分批任務時優勢明顯。

一、先搞懂:生成器函數與 yield 的基礎搭配

要使用yield,必須先定義生成器函數 —— 函數名前加*(可寫在function後或函數名前,如function* gen()或function *gen())。生成器函數調用後不會立即執行,而是返回一個生成器對象,只有調用該對象的next()方法,函數才會執行到下一個yield處暫停。

1. 基礎用法:yield 暫停與返回值
// 定義生成器函數
function* numberGenerator() {
  yield 1; // 第一次調用next(),返回1並暫停
  yield 2; // 第二次調用next(),返回2並暫停
  return 3; // 第三次調用next(),返回3並結束(done為true)
}

// 調用生成器函數,得到生成器對象(函數未執行)
const generator = numberGenerator();

// 調用next(),函數執行到第一個yield
console.log(generator.next()); 
// 輸出:{ value: 1, done: false }(done=false表示未結束)

// 再次調用next(),函數從暫停處繼續到第二個yield
console.log(generator.next()); 
// 輸出:{ value: 2, done: false }

// 第三次調用next(),函數執行完剩餘邏輯(到return)
console.log(generator.next()); 
// 輸出:{ value: 3, done: true }(done=true表示已結束)

// 已結束的生成器,後續調用next()均返回{ value: undefined, done: true }
console.log(generator.next()); 
// 輸出:{ value: undefined, done: true }

關鍵結論:

  • 每個yield對應一次next()調用,返回{ value: yield值, done: 是否結束 };

  • 生成器函數內的return值,會作為最後一次next()的value,但僅當done=true時才會返回。

2. 進階能力:yield 接收 next () 傳入的參數

yield不僅能向外返回值,還能接收外部通過next(參數)傳入的值 —— 這個參數會成為當前暫停處yield表達式的返回值。這一特性讓外部可以動態控制生成器內部的執行邏輯。

function* interactiveGenerator() {
  // 第一個yield:返回"請輸入姓名",暫停時等待外部傳入參數
  const name = yield "請輸入姓名";
  
  // 第二個yield:使用上一步接收的name,返回新提示,再次暫停
  const age = yield `你好,${name}!請輸入年齡`;
  
  // 執行到結束,返回最終結果
  return `${name},你今年${age}歲,信息已確認`;
}

const generator = interactiveGenerator();

// 第一次next():無參數,執行到第一個yield,返回"請輸入姓名"
let result = generator.next();
console.log(result.value); // 輸出:請輸入姓名

// 第二次next():傳入"張三",作為第一個yield的返回值(賦值給name)
// 執行到第二個yield,返回"你好,張三!請輸入年齡"
result = generator.next("張三");
console.log(result.value); // 輸出:你好,張三!請輸入年齡

// 第三次next():傳入25,作為第二個yield的返回值(賦值給age)
// 執行到return,返回最終結果
result = generator.next(25);
console.log(result.value); // 輸出:張三,你今年25歲,信息已確認

注意:**第一次調用****next()**時傳入的參數會被忽略,因為此時生成器還未執行到任何yield處,沒有暫停點可以接收參數。

二、yield 的核心應用場景

yield的 “暫停 - 恢復” 特性,讓它在多個場景中發揮重要作用,尤其適合處理需要 “分步執行”“動態控制” 的邏輯。

1. 處理可迭代數據流:替代數組的按需生成

當需要處理大量數據(如分頁數據、大數據流)時,直接生成完整數組會佔用大量內存,而yield可以按需生成數據,每調用一次next()返回一個數據,避免內存浪費。

// 生成1-1000的分頁數據,每次返回10條
function* paginatedDataGenerator(total, pageSize = 10) {
  let currentPage = 1;
  // 計算總頁數
  const totalPages = Math.ceil(total / pageSize);
  
  while (currentPage <= totalPages) {
    // 計算當前頁的起始和結束索引
    const start = (currentPage - 1) * pageSize + 1;
    const end = Math.min(currentPage * pageSize, total);
    
    // 生成當前頁的數據(模擬從數據庫查詢)
    const pageData = [];
    for (let i = start; i <= end; i++) {
      pageData.push(`數據${i}`);
    }
    
    // 返回當前頁數據,暫停等待下一頁請求
    yield {
      page: currentPage,
      data: pageData,
      isLast: currentPage === totalPages
    };
    
    currentPage++;
  }
}

// 使用生成器按需獲取分頁數據
const dataGenerator = paginatedDataGenerator(1000, 10);

// 獲取第1頁數據
console.log(dataGenerator.next().value);
// 輸出:{ page: 1, data: ['數據1', ..., '數據10'], isLast: false }

// 獲取第2頁數據
console.log(dataGenerator.next().value);
// 輸出:{ page: 2, data: ['數據11', ..., '數據20'], isLast: false }

// 按需獲取後續頁面...
2. 配合 async/await:處理串行異步任務

雖然async/await已成為異步編程主流,但yield結合生成器,依然可以實現更靈活的異步流程控制 —— 尤其適合需要 “前一個異步任務完成後,根據結果決定下一個任務” 的串行場景。

// 模擬異步接口:獲取用户ID
function fetchUserId() {
  return new Promise(resolve => {
    setTimeout(() => resolve("user_123"), 800);
  });
}

// 模擬異步接口:根據用户ID獲取用户信息
function fetchUserInfo(userId) {
  return new Promise(resolve => {
    setTimeout(() => resolve({ id: userId, name: "林曉", age: 28 }), 800);
  });
}

// 模擬異步接口:根據用户信息獲取訂單
function fetchUserOrders(userInfo) {
  return new Promise(resolve => {
    setTimeout(() => resolve([
      { orderId: "order_001", goods: "手機" },
      { orderId: "order_002", goods: "電腦" }
    ]), 800);
  });
}

// 生成器函數:串聯異步任務
function* asyncTaskGenerator() {
  try {
    // 第一步:獲取用户ID(等待異步完成)
    const userId = yield fetchUserId();
    // 第二步:根據ID獲取用户信息(使用上一步結果)
    const userInfo = yield fetchUserInfo(userId);
    // 第三步:根據用户信息獲取訂單(使用上一步結果)
    const orders = yield fetchUserOrders(userInfo);
    // 返回最終結果
    return { userInfo, orders };
  } catch (err) {
    // 捕獲異步任務中的錯誤
    throw new Error(`異步任務失敗:${err.message}`);
  }
}

// 執行生成器(封裝成Promise,方便使用)
function runGenerator(generator) {
  return new Promise((resolve, reject) => {
    function handleNext(result) {
      // 生成器結束,resolve最終結果
      if (result.done) {
        return resolve(result.value);
      }
      // 處理yield返回的Promise
      result.value
        .then(data => {
          // Promise成功,將結果傳入next(),繼續執行生成器
          handleNext(generator.next(data));
        })
        .catch(err => {
          // Promise失敗,通過throw()傳入錯誤,觸發生成器的catch
          handleNext(generator.throw(err));
        });
    }
    // 啓動生成器
    handleNext(generator.next());
  });
}

// 調用執行函數,獲取最終結果
runGenerator(asyncTaskGenerator())
  .then(result => {
    console.log("最終結果:", result);
    // 輸出:{ userInfo: { id: 'user_123', ... }, orders: [ {...}, {...} ] }
  })
  .catch(err => {
    console.error(err.message);
  });
3. 實現無限迭代器:動態生成循環數據

yield支持無限循環(只要不手動停止),可以實現 “無限生成數據” 的迭代器,比如生成自增 ID、循環取值等場景。

// 生成無限自增的ID
function* infiniteIdGenerator(start = 1) {
  let id = start;
  while (true) { // 無限循環,不會卡死(因為yield會暫停)
    yield id++;
  }
}

const idGenerator = infiniteIdGenerator(100);

// 按需獲取ID(需要多少取多少)
console.log(idGenerator.next().value); // 輸出:100
console.log(idGenerator.next().value); // 輸出:101
console.log(idGenerator.next().value); // 輸出:102
// 後續可繼續獲取...

三、yield 與 yield*:處理嵌套生成器

當生成器函數內部需要調用另一個生成器函數時,直接調用會返回生成器對象,而非其內部的yield值;此時需要用yield*(帶星號的 yield),它會 “委託” 給嵌套的生成器,自動遍歷其所有yield值。

// 子生成器:生成1-3的數字
function* subGenerator() {
  yield 1;
  yield 2;
  yield 3;
}

// 父生成器:使用yield*委託子生成器
function* parentGenerator() {
  yield "開始";
  yield* subGenerator(); // 委託子生成器,自動遍歷其yield值
  yield "結束";
}

const generator = parentGenerator();

console.log(generator.next().value); // 輸出:開始
console.log(generator.next().value); // 輸出:1(子生成器的第一個yield)
console.log(generator.next().value); // 輸出:2(子生成器的第二個yield)
console.log(generator.next().value); // 輸出:3(子生成器的第三個yield)
console.log(generator.next().value); // 輸出:結束

yield*的本質是 “迭代器的委託”,它會自動調用嵌套生成器的next()方法,直到其done=true,再繼續執行父生成器的邏輯。

四、使用 yield 的注意事項

  1. 作用域限制:yield僅能在生成器函數(function*)內部使用,在普通函數、箭頭函數中使用會報錯。
// 錯誤示例:普通函數中使用yield
function normalFunc() {
  yield 1; // 報錯:Uncaught SyntaxError: Unexpected number
}
  1. 箭頭函數不支持:箭頭函數不能定義為生成器函數(無法加*),因此也無法在箭頭函數中使用yield。
// 錯誤示例:箭頭函數無法作為生成器
const arrowGen = *() => { // 報錯:Uncaught SyntaxError: Unexpected token '*'
  yield 1;
};
  1. 錯誤處理:生成器內部的錯誤可以通過generator.throw(err)主動拋出,觸發內部的catch;外部也可以通過next()返回的value(Promise)的catch捕獲異步錯誤。

yield作為生成器函數的核心關鍵詞,其 “暫停 - 恢復” 和 “雙向傳值” 的特性,讓 JavaScript 在迭代處理、異步控制上有了更靈活的選擇。雖然日常開發中async/await更常用,但在處理大數據流、動態迭代邏輯時,yield依然是不可替代的工具。掌握它的基礎用法和應用場景,能讓你在面對複雜編程需求時多一種解決方案。