JavaScript生成器的使用和Python的對比
JavaScript權威指南第六版,第11章 JavaScript的子集和擴展,11.4 迭代,生成器。
下面是書中的內容抄錄,最後是Python版本的生成器實現。
/**
* 生成器
*
* 筆者注:ES6中似乎無法使用
*
* 生成器是JavaScript 1.7的特性(從Python中借過來的概念),這裏用到了一個新的關鍵字yield,使用這個關鍵字時代碼必須顯式
* 指定JavaScript的版本1.7。關鍵字yield和return類似,返回一個值,區別在於,使用yield的函數“產生”一個可保持函數內部狀態
* 的值,這個值是可以恢復的。這種可恢復性使得yield成爲編寫迭代器的有力工具。
*
* 任何使用關鍵字yield的函數(哪怕yield在代碼邏輯中是不可達的)都稱爲“生成器函數”(generator function)。生成器函數通過
* yield返回值。這些函數中可以使用return來終止函數的執行而不帶任何返回值,但不能使用return來返回一個值。除了使用yield,
* 對return的使用限制也使生成器函數更明顯地區別普通函數。然而和普通的函數函數一樣,生成器函數也通過關鍵字function聲明,
* typeof運算符返回“function”,並可以從Function.prototype繼承屬性和方法。但對生成器函數的調用卻和普通函數完全不一樣,
* 不是執行生成器函數的函數體,而是返回一個生成器對象。
*
* 生成器是一個對象,用以表示生成器函數的當前執行狀態。它定義了一個next()方法,後者可恢復生成器函數的執行,只要遇到下一條
* yield語句爲止。這時,生成器函數中的yield語句的返回值就是生成器的next()方法的返回值。如果生成器函數通過執行return語句
* 或者到達函數體末尾終止,那麼生成器的next()方法將拋出一個StopIteration。
*
* 只要一個對象包含可拋出StopIteration的next()方法,它就是一個迭代器對象。實際上,它們是可迭代的迭代器,也就是說,它們可以
* 通過for/in循環進行遍歷。下面的代碼展示瞭如何簡單地使用生成器函數以及對它生成的返回值進行遍歷。
*/
// 針對一個整數範圍定義一個生成器函數
function range(min, max) {
for (let i=Math.ceil(min); i<max; i++) yield i;
}
for (let n in range(3,8)) console.log(n); // 輸出數字3~8
/**
* 生成器函數不需要返回。實際上,最典型的例子就是用生成器來生成Fibonacci數列。
*/
function fibonacci() {
let x = 0, y = 1;
while(true){
yield y;
[x,y] = [y, y+x];
}
}
f = fibonacci();
for (let i=0;i<10;i++) console.log(f.next());
/**
* fibonacci()生成器函數沒有返回。因此,它所產生的生成器不會拋出StopIterator。不能使用for/in循環,這個循環是一個無窮循環,
* 而是把它當做迭代器顯式調用10次它的next()方法。這段代碼運行後,生成器f仍然保持着生成器函數的執行狀態,如果不再使用f,則可
* 以通過調用f.close()方法來釋放它。
* 調用close()之後,生成器函數就會終止執行。如果當前掛起的位置在一個或者多個try語句塊中,那麼將首先運行finally從句,在執行
* close()。close()沒有返回值,但如果finally語句塊產生了異常,這個異常則會傳播給close()。
*/
f.close();
/**
* 生成器經常用來處理序列化的數據,比如元素列表、多行文本、詞法分析器中的單詞等。生成器可以像Unix的shell命令中的管道那樣鏈式
* 使用。有趣的是,這種用法中的生成器是“懶惰的”,只有在需要的時候纔會從生成器(或者生成器的管道)中“取”值,而不是一次將許多
* 結果都計算出來。
*
* 例11-1:一個生成器管道
*/
// 一個生成器,每次產生一行字符串s
// 這裏沒有使用s.split(),因爲這樣會每次都處理整個字串,並分配一個數組
// 我們希望更“懶”一點
function eachline(s) {
let p;
while((p=s.indexOf('\n')) != -1){
yield s.substring(0, p);
s = s.substring(p+1);
}
if (s.length > 0) yield s;
}
// 一個生成器函數,對於每個可迭代的i的每個元素x,都會產生一個f(x)
function map(i, f) {
for (let x in i) yield f(x);
}
// 一個生成器函數,針對每個結果爲true的f(x),爲i生成一個元素
function select(i, f) {
for (let x in i)
if (f(x)) yield x;
}
// 準備處理這個字符串
let text = " #conmment \n \n hello \nworld\n quit \n unreached \n";
// 文本隔成行
let lines = eachline(text);
// 去掉收尾的空白字符
let trimmed = map(lines, function (line) {
return line.trim();
});
// 挑選非空行和非註釋的行
let nonblank = select(trimmed, function (line) {
return line.length > 0 && line[0] != "#"
});
// 現在從管道中取出經過刪減和篩選後的行進行處理,直到遇到“quit”的行
for (let line in nonblank) {
if (line === "quit") break;
console.log(line);
}
/**
* 生成器往往是在創建的時候初始化,傳入生成器函數的值是生成器所接收的唯一輸入。然而,也可以爲正在執行的生成器提供更多輸入。
* 每一個生成器都有一個send()方法,後者用來重啓生成器的執行,就像next()一樣。和next()不同的是,send()可以帶一個參數,這
* 個參數的值就成爲yield表達式的值(多數生成器函數是不會接收額外的輸入的,關鍵字yield看起來像一條語句。但實際上,yield是
* 一個表達式,是可以有值的)。除了next()和send()之外,還有一種方法可以重啓生成器的執行,即使用throw()。如果調用這個方法,
* yield表達式就將參數作爲一個異常拋給throw(),比如,下面一段代碼。
*/
// 一個生成器函數,用以從某個初始值開始計數
// 調用生成器的send()來進行增量計算,調用生成器的throw("reset")來重置初始值
// 這裏的代碼只是示例,throw()的這種用法並不推薦
function countor(initial) {
let nextValue = initial; //定義初始值
while(true){
try{
let increment = yield nextValue; // 產生一個值並得到增量
if (increment) { // 如果我們傳入一個增量...
nextValue += increment; // ...那麼使用它
}
else{
nextValue++; // 否則自增1
}
}
catch (e){ // 如果調用了生成器的throw(),就會執行這裏的邏輯
if (e == "reset") {
nextValue = initial;
}
else{
throw e;
}
}
}
}
let c = countor(10); // 用10來創建生成器
console.log(c.next()); // 輸出10
console.log(c.send(2)); // 輸出12
console.log(c.throw("reset")); // 輸出10
Python的生成器實例對比。
def countor(initial):
nextValue = initial
while 1:
try:
increment = yield nextValue
if increment:
nextValue += increment
else:
nextValue += 1
except Exception as e:
if str(e) == "reset":
nextValue = initial
else:
raise e
"""
運行結果:
In [2]: c = countor(10)
In [3]: next(c)
Out[3]: 10
In [4]: c.send(2)
Out[4]: 12
In [5]: c.throw(Exception("reset"))
Out[5]: 10
"""