Skip to content

对 ES6 中 Generator 的理解 #8

@Weiting-Zhang

Description

@Weiting-Zhang

一、背景:Iterator 迭代器

什么是迭代器呢?参考维基百科的解释:

迭代器(Iterator)是确使用户可在容器对象(container,例如链表或数组)上遍历的对象,设计人员使用此接口无需关心容器对象的内存分配的实现细节。

其主要用途是允许用户处理一个容器的所有元素,而将用户隔离于容器的内部结构。例如在 JavaScript 中,我们可以对以下实现了迭代器接口的任意对象使用 for...of 遍历其全部元素,而无需关心它具体是数组,还是 Map/Set/String:

  • Array
  • TypedArray
  • String
  • Map
  • Set
for (let i of [0, 1, 2]) {
    console.log(i); // 0 1 2
}

for (let s of 'hello') {
    console.log(s); // h e l l o
}

for (let element of new Set([3, 4, 5]) ){
    console.log(element); // 3 4 5
}

以上是 JS 中内置了迭代器接口的几种集合(容器对象),他们能够被迭代的原因是这些数据结构的 [Symbol.iterator] 属性上都是函数,执行后会返回一个迭代器

const arr = [0, 1, 2];
const func = arr[Symbol.iterator];  // ƒ values() { [native code] }
const it =  arr[Symbol.iterator](); ; // Array Iterator {}

返回的这个迭代器的作用是什么呢?我们发现,可以通过一直调用迭代器的 next 函数,来遍历它关联集合的全部元素:

it.next(); // {value: 0, done: false}
it.next(); // {value: 1, done: false}
it.next(); // {value: 2, done: false}
it.next(); // {value: undefined, done: true}

当我们对 JS 中默认可迭代的数据结构进行 for...of 循环操作时,实际上也是在隐式生成相应迭代器来进行遍历。
同时注意到,迭代器调用 next 的返回值里面,除了有 value 属性表示当前迭代元素的值之外,还有一个 done 属性表示当前迭代器是否被消耗完毕,符合迭代器协议。迭代器协议对迭代器的行为是这样规定的:

一个迭代器需要实现一个拥有以下语义(semantic)的 next() 方法:
next() 为一个无参数函数,执行后必须返回一个对象,该对象应当有两个属性: done(boolean),value:任何 JavaScript 值,done 为 true 时可省略。
done 的判定:如果迭代器可以产生序列中的下一个值,则为 false。如果迭代器已将序列迭代完毕,则为 true。这种情况下,value 是可选的,如果它依然存在,即为迭代结束之后的默认返回值。

JS 在默认的可迭代数据结构中都内置了迭代器的实现,但我们也可以自行根据迭代器协议来生成各种自定义集合。比如可以这样得到一个指定范围内按指定梯度递增的队列:

function makeRangeIterator(start = 0, end = Infinity, interval = 1) {
    let index = start;
    return {
       next: function() {
          const res = index;
          index+= interval;
          if (res < end) {
              return {
                   value: res,
                   done: false
               };
          } else {
               return {
                   done: true
               }
          }         
       }
    };
}

let it = makeRangeIterator(1, 10, 2);

let result = it.next();
while (!result.done) {
 console.log(result.value); // 1 3 5 7 9
 result = it.next();
}

二、Generator 生成器

通过上面的例子发现,虽然可以使用普通函数生成自定义的迭代器,非常强大&灵活,但由于需要显示维护迭代器内部的状态,这种方式并不十分友好。为此,ES6 提供了一种专门用于生成迭代器的函数:生成器函数,它允许开发者编写自定义的迭代算法,而无需手动控制 next 的返回、 value, done 等状态,而是用一种新的语法和关键字自动完成 next 函数的定义及 valuedone 的返回,编写者只需要控制其中的迭代逻辑。生成器函数使用 function* 的语法编写:

function* makeRangeIterator(start = 0, end = Infinity, step = 1) {
    for (let i = start; i < end; i += step) {
        yield i;
    }
   return 444;
}

最初被调用时,生成器函数不执行任何代码,而是返回一个迭代器:

let it = makeRangeIterator(1, 10, 2); // makeRangeIterator {<suspended>}
console.log(it.next); // ƒ next() { [native code] }

当第一次调用这个迭代器的 next 方法时,其返回值取决于生成器中遇到的第一个 yield语句, 生成器中的代码将会执行,直到执行完第一个 yield expression 语句,此时生成器函数会暂停并退出,同时会返回一个具有value, done 属性的对象(符合迭代器协议),其中 value 的值为 yield 关键字后面 expression 的计算值

it.next(); // {value: 1, done: false}

当继续调用迭代器的 next方法时,生成器函数会从刚才暂停的地方继续执行,直到遇到下一个 yield 语句。如果继续执行后发现没有 yield 语句,则会返回 { value: undefined, done: true }(如果生成器函数有 return 语句,则下一次的 value 为 return 表达式的值)

it = makeRangeIterator(1, 10, 2);

let result = it.next();
while (!result.done) {
 console.log(result.value); // 1 3 5 7 9
 result = it.next();
}
// 之后再调用 next 会一直返回 { value: undefined , done: true } 
// 无论生成器函数中是否有 return 语句
it.next();  // {value: undefined , done: true} 

三、迭代器与可迭代对象

一对比较容易混淆但又常在一起出现的概念是:迭代器与可迭代对象。一句话解释这两个的关系就是:可迭代对象是 [Symbole.iterator] 属性上实现了 next 方法(且返回值遵循迭代器协议)的对象,

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions