JavaScript生成器的使用和Python的对比
JavaScript权威指南第六版,第11章 JavaScript的子集和扩展,11.4 迭代,生成器。
下面是书中的内容抄录,最后是Python版本的生成器实现。
/**
* 生成器
*
* 笔者注:ES6中似乎无法使用
*
* 生成器是JavaScript 1.7的特性(从Python中借过来的概念),这里用到了一个新的关键字yield,使用这个关键字时代码必须显式
* 指定JavaScript的版本1.7。关键字yield和return类似,返回一个值,区别在于,使用yield的函数“产生”一个可保持函数内部状态
* 的值,这个值是可以恢复的。这种可恢复性使得yield成为编写迭代器的有力工具。
*
* 任何使用关键字yield的函数(哪怕yield在代码逻辑中是不可达的)都称为“生成器函数”(generator function)。生成器函数通过
* yield返回值。这些函数中可以使用return来终止函数的执行而不带任何返回值,但不能使用return来返回一个值。除了使用yield,
* 对return的使用限制也使生成器函数更明显地区别普通函数。然而和普通的函数函数一样,生成器函数也通过关键字function声明,
* typeof运算符返回“function”,并可以从Function.prototype继承属性和方法。但对生成器函数的调用却和普通函数完全不一样,
* 不是执行生成器函数的函数体,而是返回一个生成器对象。
*
* 生成器是一个对象,用以表示生成器函数的当前执行状态。它定义了一个next()方法,后者可恢复生成器函数的执行,只要遇到下一条
* yield语句为止。这时,生成器函数中的yield语句的返回值就是生成器的next()方法的返回值。如果生成器函数通过执行return语句
* 或者到达函数体末尾终止,那么生成器的next()方法将抛出一个StopIteration。
*
* 只要一个对象包含可抛出StopIteration的next()方法,它就是一个迭代器对象。实际上,它们是可迭代的迭代器,也就是说,它们可以
* 通过for/in循环进行遍历。下面的代码展示了如何简单地使用生成器函数以及对它生成的返回值进行遍历。
*/
// 针对一个整数范围定义一个生成器函数
function range(min, max) {
for (let i=Math.ceil(min); i<max; i++) yield i;
}
for (let n in range(3,8)) console.log(n); // 输出数字3~8
/**
* 生成器函数不需要返回。实际上,最典型的例子就是用生成器来生成Fibonacci数列。
*/
function fibonacci() {
let x = 0, y = 1;
while(true){
yield y;
[x,y] = [y, y+x];
}
}
f = fibonacci();
for (let i=0;i<10;i++) console.log(f.next());
/**
* fibonacci()生成器函数没有返回。因此,它所产生的生成器不会抛出StopIterator。不能使用for/in循环,这个循环是一个无穷循环,
* 而是把它当做迭代器显式调用10次它的next()方法。这段代码运行后,生成器f仍然保持着生成器函数的执行状态,如果不再使用f,则可
* 以通过调用f.close()方法来释放它。
* 调用close()之后,生成器函数就会终止执行。如果当前挂起的位置在一个或者多个try语句块中,那么将首先运行finally从句,在执行
* close()。close()没有返回值,但如果finally语句块产生了异常,这个异常则会传播给close()。
*/
f.close();
/**
* 生成器经常用来处理序列化的数据,比如元素列表、多行文本、词法分析器中的单词等。生成器可以像Unix的shell命令中的管道那样链式
* 使用。有趣的是,这种用法中的生成器是“懒惰的”,只有在需要的时候才会从生成器(或者生成器的管道)中“取”值,而不是一次将许多
* 结果都计算出来。
*
* 例11-1:一个生成器管道
*/
// 一个生成器,每次产生一行字符串s
// 这里没有使用s.split(),因为这样会每次都处理整个字串,并分配一个数组
// 我们希望更“懒”一点
function eachline(s) {
let p;
while((p=s.indexOf('\n')) != -1){
yield s.substring(0, p);
s = s.substring(p+1);
}
if (s.length > 0) yield s;
}
// 一个生成器函数,对于每个可迭代的i的每个元素x,都会产生一个f(x)
function map(i, f) {
for (let x in i) yield f(x);
}
// 一个生成器函数,针对每个结果为true的f(x),为i生成一个元素
function select(i, f) {
for (let x in i)
if (f(x)) yield x;
}
// 准备处理这个字符串
let text = " #conmment \n \n hello \nworld\n quit \n unreached \n";
// 文本隔成行
let lines = eachline(text);
// 去掉收尾的空白字符
let trimmed = map(lines, function (line) {
return line.trim();
});
// 挑选非空行和非注释的行
let nonblank = select(trimmed, function (line) {
return line.length > 0 && line[0] != "#"
});
// 现在从管道中取出经过删减和筛选后的行进行处理,直到遇到“quit”的行
for (let line in nonblank) {
if (line === "quit") break;
console.log(line);
}
/**
* 生成器往往是在创建的时候初始化,传入生成器函数的值是生成器所接收的唯一输入。然而,也可以为正在执行的生成器提供更多输入。
* 每一个生成器都有一个send()方法,后者用来重启生成器的执行,就像next()一样。和next()不同的是,send()可以带一个参数,这
* 个参数的值就成为yield表达式的值(多数生成器函数是不会接收额外的输入的,关键字yield看起来像一条语句。但实际上,yield是
* 一个表达式,是可以有值的)。除了next()和send()之外,还有一种方法可以重启生成器的执行,即使用throw()。如果调用这个方法,
* yield表达式就将参数作为一个异常抛给throw(),比如,下面一段代码。
*/
// 一个生成器函数,用以从某个初始值开始计数
// 调用生成器的send()来进行增量计算,调用生成器的throw("reset")来重置初始值
// 这里的代码只是示例,throw()的这种用法并不推荐
function countor(initial) {
let nextValue = initial; //定义初始值
while(true){
try{
let increment = yield nextValue; // 产生一个值并得到增量
if (increment) { // 如果我们传入一个增量...
nextValue += increment; // ...那么使用它
}
else{
nextValue++; // 否则自增1
}
}
catch (e){ // 如果调用了生成器的throw(),就会执行这里的逻辑
if (e == "reset") {
nextValue = initial;
}
else{
throw e;
}
}
}
}
let c = countor(10); // 用10来创建生成器
console.log(c.next()); // 输出10
console.log(c.send(2)); // 输出12
console.log(c.throw("reset")); // 输出10
Python的生成器实例对比。
def countor(initial):
nextValue = initial
while 1:
try:
increment = yield nextValue
if increment:
nextValue += increment
else:
nextValue += 1
except Exception as e:
if str(e) == "reset":
nextValue = initial
else:
raise e
"""
运行结果:
In [2]: c = countor(10)
In [3]: next(c)
Out[3]: 10
In [4]: c.send(2)
Out[4]: 12
In [5]: c.throw(Exception("reset"))
Out[5]: 10
"""