原文: 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() 方法,该方法返回一个对象。这个对象包含 done 和 value 两个属性,前者表明是否还有更多元素,后者包含实际的元素。你可以像这样创建迭代器:
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();
被广泛使用的库,例如 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/