深入產生器函式


在〈簡介產生器函式〉中看到的產生器函式,都只使用了 yield 而沒有 return, 如果定義產生器函式的時候使用了 return 會如何?

function* range(start, end) {
    for(let i = start; i < end; i++) {
        yield i;
    }
    return 'return';
}

for 迴圈之後,程式最後使用 return 試圖傳回 'return' 字串,來看看執行的結果:

> let g = range(1, 3);
undefined
> g.next();
{ value: 1, done: false }
> g.next();
{ value: 2, done: false }
> g.next();
{ value: 'return', done: true }
>

可以看到,產生器最後迭代出來的物件中,value 特性是 'return',而 done 特性被設為 true。回想一下,對於 for...of,在看到 done 特性後就停止,因此若是在 for...of 中,並不會迭代出 return 指定的值。

> let g2 = range(1, 3);
undefined
> for(let i of g2) {
...     console.log(i);
... }
1
2
undefined
>

你可以使用產生器的 return 方法,要求產生器直接 return,例如:

> function* range(start, end) {
...     for(let i = start; i < end; i++) {
.....         yield i;
.....     }
... }
undefined
> let g = range(1, 3);
undefined
> g.next();
{ value: 1, done: false }
> g.return();
{ value: undefined, done: true }
> let g2 = range(1, 3);
undefined
> g2.next();
{ value: 1, done: false }
> g2.return(10);
{ value: 10, done: true }
>

return 方法會在先前 yield 處進行 return,此時傳回的物件 done 特性會是 truevalue 特性會是 undefined,如果 return 方法有指定引數,那麼傳回的物件 done 特性會是 truevalue 特性會是指定的引數。

類似地,產生器有個 throw 方法,它會在先前 yield 處,將 throw 方法指定的值進行 throw,例如:

> let g = range(1, 3);
undefined
> g.next();
{ value: 1, done: false }
> g.throw('Orz');
Thrown: Orz
> let g2 = range(1, 3);
undefined
> g2.next();
{ value: 1, done: false }
> g2.throw(new Error('XD'));
Error: XD
    at repl:1:10
    at ContextifyScript.Script.runInThisContext (vm.js:50:33)
    at REPLServer.defaultEval (repl.js:240:29)
    at bound (domain.js:301:14)
    at REPLServer.runBound [as eval] (domain.js:314:12)
    at REPLServer.onLine (repl.js:441:10)
    at emitOne (events.js:121:20)
    at REPLServer.emit (events.js:211:7)
    at REPLServer.Interface._onLine (readline.js:282:10)
    at REPLServer.Interface._line (readline.js:631:8)
>

產生器實作了迭代器的協定,如果你試著檢驗產生器,會發現它是產生器函式的實例,產生器的原型物件就是產生器函式的 prototype

> let g = range(1, 3);
undefined
> g instanceof range;
true
> g.__proto__ == range.prototype;
true
>

話雖如此,你不能對一個產生器函式使用 new

> let g2 = new range(1, 3);
TypeError: range is not a constructor
    at repl:1:10
    at ContextifyScript.Script.runInThisContext (vm.js:50:33)
    at REPLServer.defaultEval (repl.js:240:29)
    at bound (domain.js:301:14)
    at REPLServer.runBound [as eval] (domain.js:314:12)
    at REPLServer.onLine (repl.js:441:10)
    at emitOne (events.js:121:20)
    at REPLServer.emit (events.js:211:7)
    at REPLServer.Interface._onLine (readline.js:282:10)
    at REPLServer.Interface._line (readline.js:631:8)
>

rangeprototype 上,constructor 特性也不是參考 range

> range.prototype.constructor === range;
false
>

由於不能將產生器函式當成是建構式,因此在產生器函式中撰寫 this 的意義並不大,因為直接呼叫函式的話,this 會是 undefined

> function* range(start, end) {
...     for(this.i = start; this.i < end; this.i++) {
.....         yield this.i;
.....     }
... }
undefined
> let g = range(1, 3);
undefined
> g.next();
TypeError: Cannot set property 'i' of undefined
    at range (repl:2:16)
    at range.next (<anonymous>)
    at repl:1:3
    at ContextifyScript.Script.runInThisContext (vm.js:50:33)
    at REPLServer.defaultEval (repl.js:240:29)
    at bound (domain.js:301:14)
    at REPLServer.runBound [as eval] (domain.js:314:12)
    at REPLServer.onLine (repl.js:441:10)
    at emitOne (events.js:121:20)
    at REPLServer.emit (events.js:211:7)
>

雖然說,可以使用 call 方法來指定 this

> let o = {};
undefined
> let g = range.call(o, 1, 3);
undefined
> g.next();
{ value: 1, done: false }
> o.i;
1
> g.next();
{ value: 2, done: false }
> o.i;
2
> g.i;
undefined
>

然而,this 綁定的物件跟產生器沒有關係,若你真的有相似的需求,何不直接讓它明確一點呢?

> function* range(obj, start, end) {
...     for(obj.i = start; obj.i < end; obj.i++) {
.....         yield obj.i;
.....     }
... }
undefined
> let o = {};
undefined
> let g = range(o, 1, 3);
undefined
> g.next();
{ value: 1, done: false }
> o.i;
1
> g.next();
{ value: 2, done: false }
> o.i;
2
>