1. 程式人生 > >JS中的迭代器和生成器

JS中的迭代器和生成器

迭代器

定義

迭代器iterator是一個object,這個object有一個next函式,該函式返回一個有value和done屬性的object,其中value指向迭代序列中的下個值。這樣看來迭代器定義簡直簡單的感人T_T.
例如下列code

function makeIterator(array) {
    console.log("Enter this function");
    var nextIndex = 0;
    return {
       next: function() {
           return nextIndex < array
.length ? {value: array[nextIndex++], done: false} : {done: true}; } }; } var itt = makeIterator([1,3, 5]); console.log(itt); console.log("before calling next"); console.log(itt.next().value); console.log(itt.next().value); console.log
(itt.next().value); console.log(itt.next().done);

通過執行以上程式碼,我們可以看到列印結果是:

Enter this function
Object..
before calling next
1
3
5
true

其中列印Object內容展開是

Object
next:function ()
arguments:null
caller:null
length:0
name:”next”
prototype:Object
proto

:function ()
[[FunctionLocation]]:testJS.js:31
[[Scopes]]:Scopes[2]
0:Closure (makeIterator)
array:Array(3)
nextIndex:3
1:Global

由此可以理解nextIndex和Array的值原來是存在閉包的scope裡面。這個我之前一直不太理解的說,需要補一下閉包和scope相關知識。

迭代協議 Iteration Protocol

對迭代器有了初步概念後,該是時候稍作深入一下了,也就是迭代協議。迭代協議分為兩部分:

  • 可迭代協議
  • 迭代器協議

可迭代協議(Iterable protocol)

What for?
可以認為,一旦支援可迭代協議,意味著該物件可以用for-of來遍歷,可以用來定義或者定製JS 物件的迭代行為。常見的內建型別比如Array & Map都是支援可迭代協議的。
How?
物件必須實現@@interator方法,意味著物件必須有一個帶有@@interator key的可以通過常量Symbol.iterator訪問到的屬性。[Symbol.iterator]是一個返回物件型別的零引數的函式
可以通過執行下面程式碼來理解string物件的可迭代協議:

var someString = 'hi';
console.log(someString[Symbol.iterator]);  

迭代器協議(iterator protocol)

迭代器協議就是已經在上面的迭代器一節所講到的那樣,iterator協議定義了產生value序列的一種標準方法。只要實現符合要求的next函式,該物件就是一個迭代器。

迭代協議例子

例如我們可以自定義string的迭代行為:

// need to construct a String object explicitly to avoid auto-boxing
var someString = new String('hi');

someString[Symbol.iterator] = function() {
  return { // this is the iterator object, returning a single element, the string "bye"
    next: function() {
      if (this._first) {
        this._first = false;
        return { value: 'bye', done: false };
      } else {
        return { done: true };
      }
    },
    _first: true
  };
};

生成器(generator)

瞭解了什麼是生成器之後,我們已經打好了基礎,可以進一步理解生成器。

Why need it?

儘管自定義的迭代器很有用,不過建立迭代器需要仔細地程式設計,因為這裡面需要明確地維護迭代中的內部狀態。生成器可以允許你通過寫一個可以維護自身狀態資料的函式來定義一個可迭代演算法,是迭代器的一個有力替代方案。

What is?

GeneratorFunction 是一種特殊型別的函式,該函式是作為iterator的工廠來工作的。當它得到執行的時候,會返回一個新的Generator object。

How to?

只要定義函式的時候使用 function * 語法就可以了。聽起來生成器的寫法也是簡單的感人,那麼事實是這樣麼……

code sample

function * makeGenerator(array){
    var index = 0;
    while (index <array.length)
        yield array[index++];
}

var ge = makeGenerator(["a","b","c"]);
console.log(ge);
console.log(ge.next().value);
console.log(ge.next().value);
console.log(ge.next().value);

列印ge物件可以看到其proto, GeneratorFunction 和閉包scope的內容。

  makeGenerator {[[GeneratorStatus]]: "suspended"}
        __proto__:Generator
        [[GeneratorStatus]]:"suspended"
        [[GeneratorFunction]]:function* makeGenerator(array)
        [[GeneratorReceiver]]:Window
        [[GeneratorLocation]]:testJS.js:53
        [[Scopes]]:Scopes[2]
            0:Closure (makeGenerator)
                array:Array(3)
                    0:"a"
                    1:"b"
                    2:"c"
                    length:3
                    __proto__:Array(0)
                index:3
            1:Global

那麼理解了生成器的概念之後,檢視列印的ge物件的內容,請問generator到底是迭代器還是可迭代呢?答案就是both。 那麼為什麼呢?可以在展開的ge物件裡面 去找迭代協議的兩種體現:next函式 和 Symbol.iterator屬性。

可迭代

使用者自定義可迭代User-defined Iterables

理解了上述概念後,我們就可以試著自己定義可迭代的物件啦。
例如使用者定義一個可迭代的物件

var myIterable = {};
myIterable[Symbol.iterator] = function* () {
    yield 1;
    yield 2;
    yield 3;
};
for (let value of myIterable) { 
    console.log(value); 
}

內建可迭代(built-in iterables)

String, Array, TypedArray, Map and Set 物件都是內建的可迭代物件。

語法期待可迭代(Syntaxes Expecting Iterables)

不是很明白這個什麼叫syntaxes expecting,不過好像不是很耽誤。某些表示式或者語句是expecting iterables,例如下面四種

for-of 迴圈

用法如下,乍看好像跟for-in很像。

let list = [4, 5, 6];
for (let i of list) {
   console.log(i); 
}

那麼果真如此麼?
例子如下:

let list = [4, 5, 6];
for (let i in list) {
   console.log(i); // "0", "1", "2",
}
for (let i of list) {
   console.log(i); // "4", "5", "6"
}

哈,蠻神奇的,果然不一樣,這是為啥呢?
與for-in的區別: 在於for-in是遍歷物件的所有列舉屬性名字, for of 遍歷物件的迭代器的values。
喜歡用python的同學不要跟我一樣容易用混了哦~

spread操作符

多元素或者變數展開。詳見spread operator
也可以用作array的copy,但要注意copy是淺層copy哦。

yield *

yield * 被用於委派(delegate to)另外一個生成器或者可迭代物件。說起來感覺還是不如程式碼理解的直白,用程式碼理解如下:

function * g1(){
    yield 2;
    yield * 'xyz';
    console.log("after g1 y 2");
    return "this is a return of g1";
}
function *g2(){
    yield 1;
    var ret= yield * g1();
    console.log("yield * g1 = "+ret);
    yield 3;
}
var ii = g2();
console.log(ii.next().value);
console.log(ii.next().value);
console.log(ii.next().value);
console.log(ii.next().value);
console.log(ii.next().value);
console.log(ii.next().value);

結果輸出是:

1
2
x
y
z
after g1 y 2
yield * g1 = this is a return of g1
3

yield * g1( ),表示當前迭代被委派到g1()這個生成器上,繼續執行執行 yield 2,然後 遇到yield * ‘xyz’,因為 ‘xyz’可迭代,然後迭代繼續被委派,於是得到x, y , z, 執行完後返回g1這個生成器向後執行,輸出g1( ) 返回值後,yield 3.

如果把上面程式碼的yield * ‘xyz’ 改成yield ‘xyz’,就可以感受下yield vs. yield * 了。

1
2
xyz
after g1 y 2
yield * g1 = this is a return of g1
3

析構賦值(Destructuring assignment, available in ES6)

用於將陣列的多個元素或者物件的屬性解包(unpack)到不同變數。例如:

var a, b, rest;
[a, b] = [10, 20];
var o = {p: 42, q: true};
var {p, q} = o;

除此之外還有其他用途,例如變數交換:

var a = 1;
var b = 3;
[a, b] = [b, a]; 

用這個做交換,無需額外建立臨時變數,那麼到底它是如何實現的呢(不是XOR trick)?
有了解的可以分析一下不?

高階生成器

Generator.prototype.next()

生成器是按需來計算yielded的值的。next()方法也可以接受一個value,這個value可以用來修改生成器的中間狀態。傳遞給next() 的value就會被當作是上一個yield 表示式返回的結果。
舉斐波那契生成器的例子,使用next(x) 來重啟這個序列。

function* fibonacci() {
  var fn1 = 0;
  var fn2 = 1;
  while (true) {  
    var current = fn1;
    fn1 = fn2;
    fn2 = current + fn1;
    var reset = yield current;
    if (reset) {
        fn1 = 0;
        fn2 = 1;
    }
  }
}
var sequence = fibonacci();
console.log(sequence.next().value);     // 0
console.log(sequence.next().value);     // 1
console.log(sequence.next().value);     // 1
console.log(sequence.next().value);     // 2
console.log(sequence.next().value);     // 3
console.log(sequence.next().value);     // 5
console.log(sequence.next().value);     // 8
console.log(sequence.next(true).value); // 0
console.log(sequence.next().value);     // 1
console.log(sequence.next().value);     // 1
console.log(sequence.next().value);     // 2

Generator.prototype.return()

返回給定的值並且終結生成器。

function* gen() { 
  yield 1;
  yield 2;
  yield 3;
}

var g = gen();

g.next();        // { value: 1, done: false }
g.return('foo'); // { value: "foo", done: true }
g.next();        // { value: undefined, done: true }

Generator.prototype.throw()

The throw() method resumes the execution of a generator by throwing an error into it and returns an object with two properties done and value.
我理解是跟next(value)有點像,只不過是向裡面傳遞了一個exception,然後使得該生成器裡面觸發異常並返回一個帶有done和value屬性的object。
返回的done值根據執行時迭代器是否done而取true或者false。
例子如下:

function* gen() {
  while(true) {
    try {
       yield 42;
    } catch(e) {
      console.log('Error caught!');
    }
  }
}

var g = gen();
console.log(g.next());
console.log(g.throw(new Error('Something went wrong')));
console.log(g.next());

列印結果是:

Object {value: 42, done: false}
Error caught!
Object {value: 42, done: false}
Object {value: 42, done: false}