迭代器和生成器


Mozilla 的 JavaScript 扩展引入了一些新的迭代机制,包括 for/each 循环和 Python 风格的迭代器和生成器。

for/each循环

for/each 循环是由 E4X 规范(ECMAScript for XML)定义的一种新的循环语句。E4X是语言的扩展,它允许 JavaScript 程序中直接出现 XML 标签,并添加了操作 XML 数据的语法和 API,Web 浏览器大多没有实现 E4X,但是 Mozilla 的 JavaScript 1.6 是支持 E4X 的。

for/each 循环和 for/in 很像,但 for/each 并不遍历对象的属性,而是遍历属性的值:

let o = {one: 1, two: 2, three: 3};
for (let p in o)
    console.log(p);
for each (let v in o)
    console.log(v);

除了遍历对象属性值外,for/each 还可以用于遍历数组元素值。

注:从 Firefox 57 开始,废弃了对 for/each 循环的支持。

迭代器

迭代器是一个对象,这个对象允许对它的值进行遍历,并保持必要的状态以便可以跟踪到当前遍历的「位置」。

迭代器必须包含 next() 方法,每次调用 next() 都会返回集合中的下一个值。例如下面的 counter() 函数返回一个迭代器:

function counter(start) {
  let nextValue = Math.round(start);
  return {next: function () {
    return nextValue++;
  }};
}

let serialNumberGenerator = counter(1000);
let sn1 = serialNumberGenerator.next();  // 1000
let sn2 = serialNumberGenerator.next();  // 1001

当迭代有限集合时,遍历完所有值并且没有多余的值可迭代时,再调用 next() 方法会抛出 StopIteration(JavaScript 1.7 的全局对象属性)。

通常,我们并不经常使用迭代器对象,而是使用可迭代的对象。可迭代的对象表示一组可迭代处理的值,必须定义一个名为 __iterator__() 的方法,用以返回这个集合的迭代器。

JavaScript 1.7 对 for/in 循环进行了扩展以便支持遍历可迭代对象。如果指定对象是可迭代的,那么 for/in 循环会自动调用其 __iterator__() 方法来获得迭代器对象,然后调用迭代器的 next() 方法,将返回值赋给循环变量,for/in 循环会自己处理 StopIteration 异常,并且处理过程对开发者不可见。下面的代码定义了一个 range() 函数来返回一个可迭代对象:

// 返回一个可迭代对象
function range(min, max) {
  return {
    get min() {return min;},
    get max() {return max;},
    includes: function (x) {
      return min <= x && x <= max;
    },
    toString: function () {
      return "[" + min + "," + max + "]";
    },
    __iterator__: function () {
      let val = Math.ceil(min);
      return {
        next: function () {
          if (val > max)
            throw StopIteration;
          return val++;
        }
      };
    }
  };
}

// 对上述区间值进行迭代
for (let i in range(1, 10))
  console.log(i);

如果你想从可迭代的对象中显式获取一个迭代器对象,只需调用 Iterator() 函数即可(这是一个定义在 JavaScript 1.7 中的全局函数),如果传入这个函数的参数是一个可迭代对象,那么它将返回这个对象的 __iterator__() 方法的调用结果。

引入 Iterator() 函数的另一个重要目的是当传入的对象或数组没有定义 __iterator__() 方法,它会返回这个对象的一个可迭代的自定义迭代器,这意味着将 Iterator() 函数和解构赋值一起使用,可以方便地对对象或数组的属性和值进行遍历:

for (let [k,v] in Iterator({a:1,b:2}))
    console.log(k + " = " + v); 

此外,Iterator() 函数返回的迭代器还有两个重要的特性,第一,它只遍历自有属性而忽略继承属性;第二。如果 Iterator() 函数传入第二个参数 true,返回的迭代器只对属性名进行遍历,而忽略属性值:

o = {x:1, y:2};
Object.prototype.z = 3;
for (p in o) console.log(p);    // 输出 x、y、z
for (p in Iteration(o, true)) console.log(p);  // 只输出 x、y

生成器

生成器是 JavaScript 1.7 中的特性(从 Python 借用过来),这里用到了一个新的关键字 yield,使用这个关键字时代码必须显式指定 JavaScript 的版本 1.7。yield 关键字在函数内使用,用法和 return 类似,用于从函数中返回值,两者的区别在于,使用 yield 的函数产生一个可以保持函数内部状态的值,这个值是可以恢复的,这种恢复性使得 yield 成为编写迭代器的有力工具。

任何使用 yield 的函数都称为「生成器函数」,生成器函数通过 yield 返回值,这些函数可以通过 return 来终止函数的执行,但不能通过 return 返回值。和普通函数一样,生成器函数也通过关键字 function 进行声明,typeof 运算符返回「function」,并可以从 Function.prototype 继承属性和方法。但对生成器函数的调用和普通函数完全不一样,不是执行生成器函数的函数体,而是返回一个生成器对象。

生成器是一个对象,表示生成器函数的当前执行状态,它定义了一个 next() 方法,后者可以恢复生成器函数的执行,直到遇到下一条 yield 语句为止,这时,生成器函数中 yield 的返回值就是生成器的 next() 方法的返回值。如果生成器函数通过执行 return 语句或者到达函数体末尾终止执行,那么生成器的 next() 方法将抛出一个 StopIteration 异常。

实际上,生成器如果包含可抛出 StopIterationnext() 方法,则同时也是可迭代的迭代器,可以通过 for/in 进行循环遍历。

下面,我们来看一些生成器函数的例子。我们可以通过生成器来定义斐波那契数列:

function fabonacci() {
  let x = 0, y = 1;
  while (true) {
    yield y;
    [x, y] = [y, x + y];
  }
}

// 调用生成器函数获取一个生成器
f = fabonacci();
for (let i = 0; i < 10; i++) {
  console.log(f.next());
}

因为这个生成器是一个无限循环,所以不能直接调用 for/in 进行遍历。如果不再使用 f,可以通过调用如下方法将其释放:

f.close();

调用生成器的 close() 方法时,和它相关的生成器就会终止执行,就像在普通函数中执行一条 return 语句。

生成器通常用来处理序列化数据,比如元素列表、多行文本、词法分析器中的单词等,生成器可以像 Unix 的 shell 命令那样链式调用,这种用法中的生成器是「懒惰的」,只有在需要的时候才会从生成器中取值,而不是一次性将所有结果都计算出来:

// 一个生成器,每次都返回一行字符串s
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 = " #comment \n \n hello \n world\n quit \n unreached \n";

// 现在创建一个生成器管道来处理它
// 首先,将文本分隔成行
let lines = eachline(text);
// 然后,去掉行首和行尾的空格
let trimmed = map(lines, function (line) {
  return line.trim();
});
// 最后,忽略空行和注释
let notblank = select(trimmed, function (line) {
  return line.length > 0 && line[0] !== '#comment';
});
// 现在,从管道中取出经过删减和筛选后的行进行处理
// 直到遇到"quit"行退出
for (let line in notblank) {
  if (line === 'quit')
    break;
  console.log(line);
}

在生成器执行期间还可以通过传入参数与生成器进行通信,每个生成器都有一个 send() 方法,用来重启生成器的执行,就像 next() 方法一样,与 next() 方法不同的是,send() 可以带一个参数,这个参数的值就成为 yield 表达式的值,除此之外,还有一种方法可以重启生成器的执行,即 throw()。如果调用这个方法,yield 表达式就将参数作为一个异常抛给 throw()

// 一个生成器函数,用于从某个初始值开始计数
function counter(initial) {
  let nextValue = initial;
  while (true) {
    try {
      let increment = yield nextValue;
      if (increment)
        nextValue += increment;
      else
        nextValue++;
    } catch (e) {
      if (e === "reset")
        nextValue = initial;
      else
        throw e;
    }
  }
}

let c = counter(10);
console.log(c.next());    // 输出 10
console.log(c.send(2));   // 输出 12
console.log(c.throw('reset')); // 输出 10

数组推导

JavaScript 1.7 中的数组推导也是从 Python 中借用过来的一个概念,它是一种利用另外一个数组或可迭代对象来初始化数组元素的技术。下面这段代码展示了数组推导的写法,这里用到了前面定义的 range() 函数,用以初始化一个数组,数组成员是 0~100 之间的偶平方数:

let evensquares = [x * x for (x in range(0, 10)) if (x % 2 === 0)];

一般来讲,数组推导的语法如下:

[expression for (variable in object)] if (condition)

再来看一些例子:

data = [2, 3, 4, -5];
// 求数组中每个元素的平方
squares = [x * x for each (x in data)];
// 求数组中非负数的平方根
roots = [Math.sqrt(x) for each (x in data) if (x > 0)];

// 将一个对象中的属性名放入新创建的数组
o = {a:1, b:2, f:function () {}};
let allkeys = [p for (p in o)];
let ownkeys = [p for (p in o) if (o.hasOwnProperty(p))];
let notfuncs = [k for ([k,v] in Iterator(o)) if (typeof v !== 'function')]; 

生成器表达式

在 JavaScript 1.8 中,将数组推导中的方括号替换成圆括号,它就成了一个生成器表达式,生成器表达式和数组推导非常类似,只不过返回的是一个生成器对象,而不是数组。和数组推导相比,使用生成器表达式的好处是可以惰性求值,从而提高程序性能,此外还可以应用于潜在的无穷序列。当然,这种方式也有缺点,那就是只能对值进行顺序存取而不能随机存取,为了得到第n个值,必须遍历它之前的n-1个元素。

有了生成器表达式,就可以极大简化之前编写的生成器管道代码:

let lines = eachline(text);
let trimmed = (l.trim() for (l in lines));
let notblank = (l for (l in trimmed) if (l.length > 0 && l[0] != '#'));

点赞 取消点赞 收藏 取消收藏

<< 上一篇: 解构赋值

>> 下一篇: 函数简写与多 catch 语句