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 的注意事項
- 作用域限制:yield僅能在生成器函數(function*)內部使用,在普通函數、箭頭函數中使用會報錯。
// 錯誤示例:普通函數中使用yield
function normalFunc() {
yield 1; // 報錯:Uncaught SyntaxError: Unexpected number
}
- 箭頭函數不支持:箭頭函數不能定義為生成器函數(無法加*),因此也無法在箭頭函數中使用yield。
// 錯誤示例:箭頭函數無法作為生成器
const arrowGen = *() => { // 報錯:Uncaught SyntaxError: Unexpected token '*'
yield 1;
};
- 錯誤處理:生成器內部的錯誤可以通過generator.throw(err)主動拋出,觸發內部的catch;外部也可以通過next()返回的value(Promise)的catch捕獲異步錯誤。
yield作為生成器函數的核心關鍵詞,其 “暫停 - 恢復” 和 “雙向傳值” 的特性,讓 JavaScript 在迭代處理、異步控制上有了更靈活的選擇。雖然日常開發中async/await更常用,但在處理大數據流、動態迭代邏輯時,yield依然是不可替代的工具。掌握它的基礎用法和應用場景,能讓你在面對複雜編程需求時多一種解決方案。