迭代器 / 生成器第一次出现在 CLU1 语言中,这门语言是由 MIT (美国麻省理工大学)的 Barbara Liskov 教授和她的学生们在 1974 年至 1975 年所设计和开发出来的。这门语言虽然古老,但是却提出了很多如今被广泛使用的编程语言特性,而生成器便是其中的一个。而在 CLU 语言之后,有 Icon 语言 2、Python 语言 3、C# 语言 4 和 Ruby 语言 5 等都受 CLU 语言影响,实现了生成器的特性。在 CLU 语言和 C# 语言中,生成器被称为迭代器(Iterator ),而在 Ruby 语言中称为枚举器(Enumerator )。迭代器(iterator)是一种对象,它能够用来遍历标准模板库容器中的部分或全部元素,每个迭代器对象代表容器中的确定的地址。迭代器修改了常规指针的接口,所谓迭代器是一种概念上的抽象:那些行为上像迭代器的东西都可以叫做迭代器。然而迭代器有很多不同的能力,它可以把抽象容器和通用算法有机的统一起来。迭代器是一个更抽象的概念,任何对象,如果它的类有 next 方法(next python3) 和iter方法返回自己本身。每个生成器都是一个迭代器,但是反过来不行。通常生成器是通过调用一个或多个 yield 表达式构成的函数 s 生成的。同时满足迭代器的定义。当你需要一个类除了有生成器的特性之外还要有一些自定义的方法时,可以使用自定义的迭代器,一般来说生成器更方便,更简单。
生成器是一种迭代器,是一种特殊的函数,使用 yield 操作将函数构造成迭代器。从计算机科学角度上看,生成器是一种类协程或半协程(Semi-coroutine ),生成器提供了一种可以通过特定语句或方法来使生成器的执行对象(Execution )暂停,而这语句一般都是 yield。普通的函数有一个入口,有一个返回值;当函数被调用时,从入口开始执行,结束时返回相应的返回值。生成器定义的函数,有多个入口和多个返回值;对生成器执行 next() 操作,进行生成器的入口开始执行代码,yield 操作向调用者返回一个值,并将函数挂起;挂起时,函数执行的环境和参数被保存下来;对生成器执行另一个 next() 操作时,参数从挂起状态被重新调用,进入上次挂起的执行环境继续下面的操作,到下一个 yield 操作时重复上面的过程。Python 的循环操作与 C 语言的实现不同,如果使用 List 等数据结构需要耗费大量的内容;循环操作中使用生成器只需要在内存中实例化一个对象,可以减少内存占用,提高循环操作的执行速度。
yield与next在生成器中扮演者非常重要的角色,前者是操作符,后者则是生成器上的属性函数,二者满足如下特性:
- 生成器的语句会在外部调用
next函数时执行,即我们可以在生成器之外控制其内部操作的执行过程。 - 当生成器执行到
yield操作符时会立即执行yield之后的语句并且暂停,语句的值作为上一步next函数的返回值,其是形如{done:false, value:x}的对象。 - 继续调用
next函数会使生成器继续执行,此处next函数的参数值会作为整个yield语句的值;生成器继续执行直到再次遇到yield,或是遇到return/throw生成器就退出。 next函数的返回值具有三种情况:- 如果再次遇到
yield,next返回值中的value属性是紧接在这条yield之后的语句执行之后的返回值; - 如果遇到的是
return,那么返回对象{done:true, value}则是return的返回值; - 其他情况下,返回对象
{done:false, value:undefined};
- 如果再次遇到
我们可以通过简单的例子来验证上述流程描述:
const iter = function* gen() {
console.log(`yield ${(yield 'a' + 0)}`);
console.log(`yield ${(yield 'b' + 1)}`);
return 'c' + 2;
}();
console.log(`next:${iter.next(0).value}`); //输出 next:a0
console.log(`next:${iter.next(1).value}`); //输出 yield 1 next:b1
console.log(`next:${iter.next(2).value}`); //输出 yield 2 next:c2
第一个 next 触发生成器执行到第一个 yield ,并立即执行 'a' + 0 = 'a0' , a0 作为这次 next 的返回值;第二个带参数为 1 的 next 触发生成器继续执行,此时 第一个 yield 才返回 1 ,然后执行到 第二个 yield 并立即立即这条 yield 后面的 'b' + 1 = 'b1' , b1 作为这次 next的返回;第三个 next 执行以此类推。在其他语言中生成器也经常作为无限数组的生成工具,譬如我们可以使用生成器来实现斐波那契数列:
const f = function* fibonacci() {
let [a, b] = [0, 1];
for (;;) {
yield a;
[a, b] = [b, a + b];
}
}();
//执行三次,得到三个对象,其value值分别是0,1,1
for (let i of Array(3).keys()) {
console.log(f.next());
}