本文同步發表於隨性筆記
本系列文章討論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以前也有enumerate
1,不過行為不太一樣,也被廢棄掉了。
自幹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()
方法可以取的下一件物品。得到的值會是一個有value
和done
欄位。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()
)。
會污染的坑...
不過要注意...不使用let
或var
變數很容易污染。(在寫本文時不小心忘記...又踩了一次坑)
※ 原諒我。在沒有語法標示的編輯器裡寫...有點難檢查。
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