.next(),也可交給 for...of。.next() 會逐筆回傳 { value, done };ES2015 Generator yield Iterable Iterator

function* 是 ES2015 加入的 generator function 語法。它本身是可呼叫的建立者,不是 iterator;呼叫它後得到的 generator object 才有 .next()。
ECMAScript 規格對 Generator Objects 的定義:
A Generator is created by calling a generator function and conforms to both the iterator interface and the iterable interface.
yield 時會保留這次函式呼叫的執行狀態,會有一個暫停狀態(suspend state);.next() 推進時,才從暫停處恢復。前幾次文章有看到 iterator,for of 透過它不斷呼叫 .next() 取得下一筆資料。
若要自己製作一個可以產生 iterator ,並且符合迭代協議的函式,必須要定義入口 [Symbol.iterator] 和 返回物件的形狀至少有 next():
function createCounter() {
let current = 1;
return {
next() {
if (current > 3) return { value: undefined, done: true };
return { value: current++, done: false };
},
[Symbol.iterator]() {
return this;
},
};
}
function* countToThree() {
yield 1;
yield 2;
yield 3;
}
const counter = countToThree();
console.log(counter.next()); // { value: 1, done: false }
console.log(counter.next()); // { value: 2, done: false }
console.log(counter.next()); // { value: 3, done: false }
console.log(counter.next()); // { value: undefined, done: true }
function* 是 generator function。呼叫它時不會立刻跑完整個函式,而是拿到一個 generator object。每次 .next(),它才從上一次暫停的位置繼續,直到下一個 yield。
因此 yield 2 不是「把整個函式回傳出去」,而是交出這一次 iterator result 的 value,並暫停目前的執行位置。下一次 .next() 會從這裡往下走。current、區域變數和迴圈進度由 generator 自己保存;比手寫狀態機容易閱讀~。
yield 像可暫停的導覽員,也能接收下一次 .next()可以把 generator 想成一位帶團導覽員。它走到 yield 就停下來,把眼前這一站交給外部,並記住自己走到哪裡、手上拿了什麼、接下來該往哪裡走。外部下一次呼叫 .next(),才說「繼續」,導覽員便從原地接著走,而不是從第一站重來。
JavaScript 把這個被保留下來的「工作現場」稱為 execution context:
其中包含目前位置、區域變數、參數與迴圈進度。讀文章時先記住「暫停後還能原地續走」即可;不必把它想成多個函式或多份狀態。
控制權可以想成這樣交接:
caller 呼叫 .next()
↓
導覽員從原地恢復
↓
流程跑到 yield
↓
暫停並交回 { value, done }
yield 不只把值交給外部,也能接收下一次 .next(value) 傳回的值:
第二次呼叫的 "讀者",會成為前一個 yield "你的名字是?" expression 的值,因此被指定給 name。
function* conversation() {
const name = yield "你的名字是?";
yield `你好,${name}`;
return "對話結束";
}
const chat = conversation();
console.log(chat.next()); // { value: "你的名字是?", done: false }
console.log(chat.next("讀者")); // { value: "你好,讀者", done: false }
console.log(chat.next()); // { value: "對話結束", done: true }
yield 不會建立新的工作現場;它暫停的是這一個 generator object 正在進行的函式呼叫。下一次 .next(value) 會恢復同一個工作現場,並把 value 交給剛才那個 yield expression。這對應TC39 規格中的 GeneratorResume 與 GeneratorYield。
發現這個暫停現象會保存上一次執行的狀態有點微妙,讓我想起 閉包(Closure)
Closure 與 Generator 都可能讓區域變數在稍後仍可使用,但它們保存狀態的原因不同:
Closure 保留對 lexical environment 的參照,外層函式可以早已執行完畢。Generator Object 保留尚未完成呼叫的暫停點、區域 bindings 與控制流程。兩者不是互斥關係,generator function 內也可以建立 Closure ,像下面範例:
function* demo() {
let count = 1;
const readCount = () => count; // Closure:捕捉 count
yield readCount;
}
Closure,因為它保留了對 count 所在 lexical environment 的參照。
一般 iterator 的必要介面只有 .next() 和 [Symbol.iterator]() 當作迭代調用入口:它只保證能回答「下一筆是什麼」和可以迭代調用即可。
.return() 與 .throw() 是 Iterator Protocol 的可選方法,不是每個 iterator 都有;它們也不是只能由語言在內部使用的隱藏方法。若物件有實作,就能直接呼叫。
Generator object 則標準地提供這三個方法。把它延續成剛才的導覽員比喻:
外部不只能說「繼續」,還能說「今天到這裡」或「這裡發生了一個錯誤」。
| 呼叫 | 外部送進 generator 的指令 | 通常會看到的結果 |
|---|---|---|
.next(value) |
正常繼續;value 會成為上一個 yield expression 的值 |
跑到下一個 yield 時回傳 { value, done: false };執行完則 done: true |
.return(value) |
要求在目前暫停點提早結束 | 先跑 finally,通常回傳 { value, done: true } |
.throw(error) |
在目前暫停點把 error 丟回 generator 內 |
catch 可處理它並繼續 yield;未處理時會在呼叫端拋出 |
因此,較精準的說法不是「Generator 比 iterator 多了一個 return()」,而是:一般 iterator 最少只要有 .next();Generator 則提供 .next()、.return()、.throw() 這套雙向流程控制介面。
.return(value):外部要求提早收工.return() 可以直接呼叫。它不是 generator function 裡寫的 return,而是外部從暫停點送入一個「請結束」的要求。
正常情況下,generator 不會再執行原本下一行的工作,而是先執行 finally,再以 done: true 結束;傳入的 value 會成為最後的 value。
function* gen() {
try {
yield 1;
yield 2;
} finally {
console.log("cleanup");
}
}
const iterator = gen();
iterator.next(); // { value: 1, done: false }
iterator.return("stop"); // 印出 cleanup,回傳 { value: "stop", done: true }
這是清理資源的重要入口,例如停止讀取、關閉連線或釋放暫存狀態。不過「通常」二字不能省略:如果 finally 裡又有 yield,清理流程本身可以再暫停一次;此時第一次 .return() 可能先得到 { done: false },下一次恢復後才真正結束。
return 與外部的 .return() 不同前面介紹的 .return() 是外部要求 generator 提早收工;這一節的 return 則是 generator function 自己執行到結束點。兩者最後都會得到 { value, done: true },但發動者不同。
就像普通 function 一樣,generator function 執行到內部的 return 時會結束。後面即使還有 yield,也不會再執行:
function* demo() {
yield 1;
return "結束";
yield 2; // 永遠不會執行
}
const iterator = demo();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: "結束", done: true }
因此,上面 conversation() 裡的 return "對話結束" 代表它自己走到流程終點。這個最終 value 可以由手動 .next() 讀到;但 for...of 只處理每次 yield 交出的值,會略過這個結束時的 value。
.throw(error):從暫停點送入錯誤.throw(error) 會讓剛才暫停的 yield 像在 generator 內部拋出了那個錯誤一樣。因此 generator 可以用 try...catch 決定要處理它、交出替代值、正常結束,或把錯誤繼續往外拋;不論如何,只要離開 scope,都會先經過 finally。
簡單來說,.throw(error) 會讓錯誤在暫停的 yield 位置發生。若這個位置被 Generator 內部的 try...catch 包住,流程就會跳進 catch;處理完成後,可以繼續 yield、正常結束或再次拋出錯誤。
function* task() {
try {
yield "等待資料";
} catch (error) {
yield `已處理:${error.message}`;
} finally {
console.log("cleanup");
}
return "結束";
}
const iterator = task();
console.log(iterator.next());
// { value: "等待資料", done: false }
console.log(iterator.throw(new Error("連線失敗")));
// { value: "已處理:連線失敗", done: false }
console.log(iterator.next());
// 印出 cleanup
// { value: "結束", done: true }
剛才的 counter 有 .next(),所以它是 iterator;它也能直接交給 for...of:
for (const value of countToThree()) {
console.log(value);
}
// 1
// 2
// 3
這表示 generator object 同時也是 iterable。概念上,它的 [Symbol.iterator]() 會回傳自己:這個物件既能回答「下一筆是什麼」,也能回答「從這裡開始迭代要用哪個 iterator」。
但要注意「每次呼叫 generator function 都會產生新的 generator」:
const first = countToThree();
const second = countToThree();
console.log(first === second); // false
first 讀到第二筆不會影響 second。這和 Array 很不一樣:Array 通常是可重複從頭迭代的 collection;單一 generator 則有自己的讀取位置,讀過的值不會自動回來。
因為 consumer 每次呼叫 .next(),generator 才會從暫停處繼續,所以很容易讓人以為「Generator 一定是 lazy」,也就是總運算次數變少,或「延後到需要時才做」。
暫停/恢復只決定何時開始或繼續執行,不限制它每次恢復後要做多少工作。
呼叫 generator function 時,函式本體確實不會立刻執行;它只會先建立 Generator Object。直到 consumer 呼叫 .next(),程式才開始執行,並在遇到下一個 yield 時暫停。
但這不代表「只要用了 Generator 或 Iterator,程式就會自動變成惰性求值」。
function* notVeryLazy(items) {
const results = items.map(expensiveWork); // 昂貴的計算
yield* results;
}

可以把 .next() 想成客人按鈴點下一道菜。廚師收到通知後,可以只做一道菜就送出,也可以先把整桌菜全部做好,最後才送出第一道。Generator 只負責「收到 .next() 才開工,遇到 yield 就暫停」;一次開工後要做多少事情,仍由程式內容決定。
function* processOneByOne(items) {
for (const item of items) {
yield expensiveWork(item);
}
}
const iterator = processOneByOne(items);
iterator.next(); // 只處理第 1 筆
iterator.next(); // 只處理第 2 筆
// 到此停止
上面這段才把工作拆在不同 yield 之間:每次 .next() 只處理目前需要的一筆,consumer 資料消費者不再索取資料時,後面的工作也不必執行,很微妙的一段相對比較。
如果函式的責任是「做一件事,最後給一個結果」,普通 function 或 async function 通常更直接:
async function loadProfile(id) {
const response = await fetch(`/api/users/${id}`);
return response.json();
}
不要因為流程裡有迴圈就急著使用 function*。Generator 適合的是 consumer 真正需要多次詢問「下一筆是什麼」的問題,例如 parser 逐個 token 交出結果、分頁資料逐頁處理,或無限序列只取前幾筆。
可以先問一句:呼叫端需要的是「一個最終結果」,還是「可自行決定何時索取下一筆的來源」?
前者選普通 function/Promise;後者才考慮 Generator。這也能避免把 Generator 當成一般非同步流程或 state 管理的替代品。
.next(),能從上一個 yield 繼續交出下一筆。yield 可接收 .next(value) 傳入的值;generator function 裡的 return 會自己結束流程。.next(),也能直接放進 for...of。.next();Generator 還有可直接呼叫的 .return()、.throw(),形成雙向流程控制介面。Generator 把狀態保存與恢復變得好寫;下一篇要問的是,consumer 不要下一筆時,producer 資料來源端是否真的不必繼續工作,它們之間有存在著什麼樣的行為關係,或者可以透過流程設計來達到?