[day 06] yield & yield*: 生成器


本文同步發表於隨性筆記

本系列文章討論JS 物件導向設計相關的特性。 不含CSS,不含HTML
建議先有些JS基礎再繼續閱讀。
你也可以看看從零開始遲來的Web開發筆記
雖然是「7天寫作松」挑戰,但同樣可以視為系列後續文章

No CSS! No HTML! No Browser!
Just need programming language


今天會往物件導向外頭邁出一步。是的,到昨天已經差不多把JS物件導向介紹的差不多了。那今天的主題是什麼呢?生成器(generator)。這個類型的建立與使用,和普通的JS類別有些不同,來看看吧!

生成器(generator)

什麼是生成器(generator)?簡單說就是一個 序列工廠 ,你跟他要東西他就給你東西,直到原料不足無法生產。

function *g1(){
    let products = ["Apple", "Banana", "Orange"]
    for(var i in products){
        yield products[i]
    }
}

for(product of g1()){
    console.log(product);
}
/* Output:
Apple
Banana
Orange
*/

語法:星號

注意到函式名稱前加了個星號(*)了嗎?然後yield配合上當下產生的產品。That's right! 就是這麼簡單。

生成器可以用在for-of迴圈裡,上面例子看起來沒什麼用。不過其實可以做到很多事情(下面還有很多例子),譬如Python裡可以透過enumerate同時取得陣列的位置與元素。

arr = ["a", "b", "c"]
for i, c in enumerate(arr):
    print(i, c)

''' Output:
0 a
1 b
2 c
'''

※ JS以前也有enumerate1,不過行為不太一樣,也被廢棄掉了。

自幹enumerate

透過生成器和解構賦值(Destructuring assignment)2也可以做到類似的事情:

function *enumerate(arr){
    for(i in arr){
        yield [i, arr[i]];
    }
}

var arr = ["Apple", "Banana", "Orange"];
for(let [i, c] of enumerate(arr)){
    console.log(i, c);
}

/* Output:
0 Apple
1 Banana
2 Orange
*/

※ 和Python一樣,除非你知道你在做什麼,不要在迭代裡更新陣列。

來細看生成器:next()

var obj = g1();
console.log(obj); // => Object [Generator] {}
console.dir(obj.next()); // => { value: 'Apple', done: false }
console.dir(obj.next()); // => { value: 'Banana', done: false }
console.dir(obj.next()); // => { value: 'Orange', done: false }
console.dir(obj.next()); // => { value: undefined, done: true }
console.dir(obj.next()); // => { value: undefined, done: true }

生成器物件有next()方法可以取的下一件物品。得到的值會是一個有valuedone欄位。value就是預取得的物件,done在檢查是不是到盡頭沒材料了。知道就可以繼續看下面內容了。

無限數列

你可能在Haskell看過無限數列([1..]):

take 10 [1..]
-- => [1,2,3,4,5,6,7,8,9,10]

用生成器也做得到:

function *naturalNumber(){
    var n = 0;
    while(true){
        n++;
        yield n;
    }
}

var natural_number = naturalNumber();
console.log(natural_number.next().value); // => 1
console.log(natural_number.next().value); // => 2
console.log(natural_number.next().value); // => 3
console.log(natural_number.next().value); // => 4

該死的整數

不過是無限整數正列的話...可能會出錯,JS只保證在±2**53保證正確而已(Number.MAX_SAFE_INTEGER)。

function *fxckNaturalNumber(){
    var n = Number.MAX_SAFE_INTEGER;
    while(true){
        n++;
        yield n;
    }
}

var fxck_natural_number = fxckNaturalNumber();
console.log(fxck_natural_number.next().value); // => 9007199254740992
console.log(fxck_natural_number.next().value); // => 9007199254740992
console.log(fxck_natural_number.next().value); // => 9007199254740992
console.log(fxck_natural_number.next().value); // => 9007199254740992

Oops!全都一樣,根本沒進展阿!

該死的整數,解決他吧!

後來有了大整數BigInt

function *veryBigInt(){
    var n = Number.MAX_SAFE_INTEGER;
    n = BigInt(n)
    while(true){
        n++;
        yield n;
    }
}

var very_big_number = veryBigInt();
console.log(very_big_number.next().value); // => 9007199254740992n
console.log(very_big_number.next().value); // => 9007199254740993n
console.log(very_big_number.next().value); // => 9007199254740994n
console.log(very_big_number.next().value); // => 9007199254740995n

fix!

實現take

剛剛看過Haskell的take:

take 5 [ (i) | i <- [1..], mod i 2 == 0 ]
-- => [2,4,6,8,10]

或者kotlin的會比較好理解:

val arr = 1..100
arr.take(10)
// res7: kotlin.collections.List<kotlin.Int> = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

來自幹吧!

veryBigInt.__proto__.prototype.take = function(n){
    var result = []
    for(var i = 0; i < n; i++){
        result.push(this.next().value)
    }
    return result
}

console.log(natural_number.take(5)); // => [ 5, 6, 7, 8, 9 ] // note: 前面提取過1~4了

批量生產: yeild*

有些東西沒必要由生成器自己產生,找 代工 吧!

function *g2(pre_do){
    let products = ["Taiwan", "Janpan", "France"];
    for(product of products){
        yield product;
    }
    if(pre_do)
        yield* pre_do;
}

for(product of g2(g1())){
    console.log(product);
}
/* Output:
Taiwan
Janpan
France
Apple
Banana
Orange
*/

yield*就像是委託代工給其他生成器(此處是傳入的g1())。

會污染的坑...

不過要注意...不使用letvar變數很容易污染。(在寫本文時不小心忘記...又踩了一次坑)

※ 原諒我。在沒有語法標示的編輯器裡寫...有點難檢查。

function *fxckG1(){
    products = ["Apple", "Banana", "Orange"]
    for(var i in products){
        yield products[i]
    }
}

function *fuckG2(pre_do){
    products = ["Taiwan", "Janpan", "France"];
    if(pre_do)
        yield* pre_do;
    for(product of products){
        yield product;
    }
}

for(product of fuckG2(fxckG1())){
    console.log(product);
}
/* Output:
Apple
Banana
Orange
Apple
Banana
Orange
*/

與一般物件不同之處

無法使用new Operator

new g1()
// Thrown:
// TypeError: g1 is not a constructor

參考資料

#js #javascript #EMCAScript
寫了這麼久的JS,你還在物件之前的時代嗎?只有資料、函式可以用,破破的抽象化,不會難以維護?儘管JS起初並不以物件導向設計,但透過原形鏈設計,其仍然可以具有好維護的物件導向特色。本系列從最基礎的this,深入ES6之後的class。






Related Posts

7. 延伸開發與相關挑戰

7. 延伸開發與相關挑戰

嘉信開戶紀錄 || Charles Schwab

嘉信開戶紀錄 || Charles Schwab

[JavaScript] 關於模組化、匯入、匯出

[JavaScript] 關於模組化、匯入、匯出



Sponsored



Comments