【译】无穷集合与 ES6 生成器

1380次阅读  |  发布于5年以前

原文:http://www.zcfy.cc/article/505

动机

无穷和惰性求值的集合对于许多语言来说是稀松平常之物,而它也开始被主流 JavaScript 支持。随着新的迭代器和生成器特性被广泛采用,现在你可以将它们用于你的项目中。它们拥有一些非常具体的应用场景,这些场景也许不会每天出现,但是在某些情况下它们会非常有用。这些特性非常新,然而脚本库已经开始针对它们提供最有用的操作。

在这篇文章中,你将学到这些特性的基础,以及一个特别的应用场景,在这个场景中你可能会用到这些新技术。同时,你也将学到一个可用的库,它提供了高效率使用这种集合的大多数基础操作。

无穷集合

数组

数组本质上是有限的,因为它将所有的元素存储在内存里。没有办法将它们动态构造出来,并且它们也不支持惰性求值。因此,一个像这样的构造器会导致无限循环从而不可实现:

var naturalNums = [];
    for(let i = 0;;i++){
      naturalNums.push(i);
    }

ES6 的 Proxies 可能改变了这个情况,因为它们添加了对动态 getter 的支持。你可能认为一个像这样的构造起将产生一个包含所有自然数的数组:

var naturalNums = new Proxy({},
      {get: (target, name) => {
        if (!isNaN(name)) {
          return Number(name);
        }else if (name === "length"){
          return Number.POSITIVE_INFINITY;
        }
      }
    });

它的确创建了一个类似数组的对象,这个对象返回所有的自然数,但是,不幸的是,实际上它难以使用。它缺少基本的数组方法例如 splice,这会让它不被一些库例如 Underscore.js 支持。理论上,你可以自己写一些公用函数比如 filter 和 map,但是这么做显然非主流。

迭代器

于是迭代器来拯救我们了。它们能够实现无穷集合,甚至有一个内置的语言特性来迭代它们:for...of 循环。要构造一个可迭代对象,你需要让它的 Symbol.iterator 属性返回一个迭代器。迭代器只需要一个 next() 方法,该方法返回一个对象。这个对象包含 donevalue 两个属性,前者表明是否还有更多元素,后者包含实际的元素。你可以像这样创建迭代器:

var naturalNums = {
      [Symbol.iterator]: (()=>{
        let i = 0;
        return {
          next: () => {
            return {done: false, value: i++};
          }
        }
      })
    };

于是你可以通过 for...of 循环遍历它(但别忘了终止条件,因为它是一个无穷集合!):

for(let i of naturalNums){
      if(i > 10) break; // Don"t forget to terminate!
      console.log(i);
    }

生成器

生成器只是迭代器的语法糖。生成器让你不用纠结于迭代器的定义形式,你可以将注意力集中于实现逻辑。同样的可迭代对象可以使用生成器创建:

var naturalNums = function* (){
      let i = 0;
      while(true){
        yield i++;
      }
    }

当你遍历它的时候,你需要调用它:

for(let i of naturalNums()){
      if(i > 10) break;
      console.log(i);
    }

用途

为什么你需要无穷集合?当你事先不知道需要多少个元素时,它们变得就很方便了。例如:计算前 100 个正数之和可以直截了当(这个例子使用 Underscore.js):

const sum = _.chain(_.range(1, 101))
      .reduce((memo, val) => memo + val, 0)
      .value();

但是计算前 100 个素数之和有点困难:

const sum = _.chain(_.range(1, 100000)) // what should stop be?
      .filter(isPrime)
      .first(100)
      .reduce((memo, val) => memo + val, 0)
      .value();

Gentoo 库 - 生成器工具(Generator tools)

被广泛使用的库,例如 Underscore.js,不支持迭代器。这些库基于数组和类数组对象。幸运的是,已经有许多项目填补了空白,虽然还得假以时日,但这些库正在慢慢成为主流。我发现的其中一个非常有用的库叫做 Gentoo,它提供了一些使用集合的基础公共方法,例如 filter、map 以及 reduce。原始仓库貌似被抛弃了,但是没关系,我 fork 了一个,并且又加了一些特性例如 takeWhile 和 chaining。只要将它加入项目,并通过 babel polyfill 处理生成器,你就可以用它来干活了。

浏览器支持

尽管还是一个相对新的,不广为人知的技术,浏览器对它支持得相当好。Chrome、Firebox,以及 Edge 都有适当的支持,只有 Safari 拖了后腿。但是通过 Babel 等编译器,你可以将代码转换成 ES5,如果运行时需要,就包含 polyfill。

注意!

当你使用无穷集合,总是要确保你使用一个操作来限制输出数量。否则很容易产生无限循环从而破坏了你的 app。

采用无穷的方式

使用 gentoo 库,前面的那个例子可以用一个更有效率和健壮的方式实现:

const sum =
      gentoo.chain(gentoo.range(1, Number.POSITIVE_INFINITY))
        .filter(isPrime)
        .limit(100)
        .reduce((memo, val) => memo + val, 0)
        .value()

这个解决方案中没有任何拍脑袋想出来的数(即前面那个方法里的 100000----译者注),这让代码更健壮。它的效率也更高,因为没有被浪费的操作(这是因为前面的方法里面, Underscore 要先计算出 1~100000 里面所有的素数然后取前 100 个,而这个方法用了 generator,它只需要在 .value( ) 求值的时候根据前面的 .limit( ) 得到应迭代的次数,这就是惰性求值(lazy evaluation)的好处----译者注)。

结束语

生成器已经被主流浏览器支持了,并且你可以将代码通过 Babel 编译,以支持更早期的浏览器版本。在今天,你已经可以毫无困难地使用它们,虽然它们的使用场景还非常有限,但只要你有效地使用它们,它们确实可以使你的代码可读性更好。

github repo

英文原文:https://advancedweb.hu/2016/05/31/infinite-collections-with-es6-generators/

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8