本文已经过原作者 MelkorNemesis 授权翻译。
Lazy evaluation
Lazy evaluation常被译为“延迟计算”或“惰性计算”,指的是仅仅在真正需要执行的时候才计算表达式的值。
与惰性求值相反的是及早求值(eager evaluation)及早求值,也被称为贪婪求值(greedy evaluation)或严格求值,是多数传统编程语言的求值策略。
充分利用惰性求值的特性带来的好处主要体现在以下两个方面:
- 避免不必要的计算,带来性能上的提升。
- 节省空间,使得无限循环的数据结构成为可能。
迭代器
ES6 中的迭代器使惰性求值和创建用户定义的数据序列成为可能。迭代是一种遍历数据的机制。迭代器是用于遍历数据结构元素(称为Iterable)的指针,用于产生值序列的指针。
迭代器是一个可以被迭代的对象。它抽象了数据容器,使其行为类似于可迭代对象。
迭代器在实例化时不计算每个项目的值,仅在请求时才生成下一个值。这非常有用,特别是对于大型数据集或无限个元素的序列。
可迭代对象
可迭代对象是希望其元素可被公众访问的数据结构。JS 中的很多对象都是可迭代的,它们可能不是很好的察觉,但是如果仔细检查,就会发现迭代的特征:
- new Map([iterable])
- new WeakMap([iterable])
- new Set([iterable])
- new WeakSet([iterable])
- Promise.all([iterable])
- Promise.race([iterable])
- Array.from([iterable])
还有需要一个可迭代的对象,否则,它将抛出一个类型错误,例如:
- for ... of
- ... (展开操作符)const [a, b, ..] = iterable (解构赋值)
- yield* (生成器)
JavaScript中已有许多内置的可迭代项:
String,Array,TypedArray,Map,Set。
迭代协议
迭代器和可迭对象遵循迭代协议。
协议是一组接口,并规定了如何使用它们。
迭代器遵循迭代器协议,可迭代遵循可迭代协议。
可迭代的协议
要使对象变得可迭代,它必须实现一个通过Symbol.iterator的迭代器方法,这个方法是迭代器的工厂。
使用 TypeScript,可迭代协议如下所示:
interface Iterable {
[Symbol.iterator]() : Iterator;
}
- 1.
- 2.
- 3.
Symbol.iterator]()是无参数函数。在可迭代对象上调用它,这意味着我们可以通过this来访问可迭代对象,它可以是常规函数或生成器函数。
迭代器协议
迭代器协议定义了产生值序列的标准方法。
为了使对象成为迭代器,它必须实现next()方法。迭代器可以实现return()方法,我们将在本文后面讨论这个问题。
使用 TypeScript,迭代器协议如下所示:
interface Iterator {
next() : IteratorResult;
return?(value?: any): IteratorResult;
}
- 1.
- 2.
- 3.
- 4.
IteratorResult 的定义如下:
interface IteratorResult {
value?: any;
done: boolean;
}
- 1.
- 2.
- 3.
- 4.
- done通知消费者迭代器是否已经被使用,false表示仍有值需要生成,true表示迭代器已经结束。
- value 可以是任何 JS 值,它是向消费者展示的值。
当done为true时,可以省略value。
组合
迭代器和可以可迭代对象可以用下面这张图来表示:
事例
基础知识介绍完了,接着,我们来配合一些事例来加深我们的映像。
范围迭代器
我们先从一个非常基本的迭代器开始,createRangeIterator迭代器。
我们手动调用it.next()以获得下一个IteratorResult。最后一次调用返回{done:true},这意味着迭代器现在已被使用,不再产生任何值。
function createRangeIterator(from, to) {
let i = from;
return {
next() {
if (i <= to) {
return { value: i++, done: false };
} else {
return { done: true };
}
}
}
}
const it = createRangeIterator(1, 3);
console.log(it.next());
console.log(it.next());
console.log(it.next());
console.log(it.next());
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
可迭代范围迭代器
在本文的前面,我已经提到 JS 中的某些语句需要一个可迭代的对象。因此,我们前面的示例在与for ... of循环一起使用时将不起作用。
但是创建符合迭代器和可迭代协议的对象非常容易。
function createRangeIterator (from, to) {
let i = from
return {
[Symbol.iterator] () {
return this
},
next() {
if (i <= to) {
return { value: i++, done: false }
} else {
return { done: true }
}
}
}
}
const it = createRangeIterator(1, 3)
for (const i of it) {
console.log(i)
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
无限序列迭代器
迭代器可以表示无限制大小的序列,因为它们仅在需要时才计算值。
注意不要在无限迭代器上使用扩展运算符(...),JS 将尝试消费迭代器,由于迭代器是无限的,因此它将永远不会结束。所以你的应用程序将崩溃,因为内存已被耗尽 ??
同样,for ... of 循环也是一样的情况,所以要确保能退出循环:
function createEvenNumbersIterator () {
let value = 0
return {
[Symbol.iterator] () {
return this
},
next () {
value += 2
return { value, done: false}
}
}
}
const it = createEvenNumbersIterator()
const [a, b, c] = it
console.log({a, b, c})
const [x, y, z] = it
console.log({ x, y, z })
for (const even of it) {
console.log(even)
if (even > 20) {
break
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
关闭迭代器
前面我们提到过,迭代器可以有选择地使用return()方法。当迭代器直到最后都没有迭代时使用此方法,并让迭代器进行清理。
for ... of循环可以通过以下方式更早地终止迭代:
- break
- continue
- throw
- return
function createCloseableIterator () {
let idx = 0
const data = ['a', 'b', 'c', 'd', 'e']
function cleanup() {
console.log('Performing cleanup')
}
return {
[Symbol.iterator]() { return this },
next () {
if (idx <= data.length - 1) {
return { value: data[idx++], done: false }
} else {
cleanup()
return { done: true }
}
},
return () {
cleanup()
return { done: true }
}
}
}
const it = createCloseableIterator()
for (const value of it) {
console.log(value)
if (value === 'c') {
break
}
}
console.log('\n----------\n')
const _it = createCloseableIterator();
for (const value of _it) {
console.log(value);
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 如果知道迭代器已经结束,则手动调用cleanup()函数。
- 如果突然完成,则return()起作用并为我们进行清理。
额外的内容
如果你已经做到了这一点,我们来看看一些额外的内容。
组合器
组合器是将现有可迭代对象组合在一起以创建新可迭代对象的函数。
因此,我们能够创建许多实用函数。那map或者filter呢?看看下面的代码,花一分钟时间来理解它。
function createEvenNumbersIterator() {
let value = 0;
return {
[Symbol.iterator]() {
return this;
},
next() {
value += 2;
return { value, done: false };
}
}
}
function map(fn, iterable) {
const iter = iterable[Symbol.iterator]();
return {
[Symbol.iterator]() {
return this;
},
next() {
const n = iter.next();
if (!n.done) {
return { value: fn(n.value), done: false };
} else {
return { done: true };
}
}
}
}
function filter(fn, iterable) {
const iter = iterable[Symbol.iterator]();
return {
[Symbol.iterator]() {
return this;
},
next() {
const n = iter.next();
if (!n.done) {
if (fn(n.value)) {
return { value: n.value, done: false };
} else {
return this.next();
}
} else {
return { done: true };
}
}
}
}
function take(n, iterable) {
const iter = iterable[Symbol.iterator]();
return {
[Symbol.iterator]() {
return this;
},
next() {
if (n > 0) {
n--;
return iter.next();
} else {
return { done: true };
}
}
}
}
function cycle(iterable) {
const iter = iterable[Symbol.iterator]();
const saved = [];
let idx = 0;
return {
[Symbol.iterator]() {
return this;
},
next() {
const n = iter.next();
if (!n.done) {
saved[idx++] = n.value;
return { value: n.value, done: false };
} else {
return { value: saved[idx++ % saved.length], done: false };
}
}
}
}
function collect(iterable) {
// consumes the iterator
return Array.from(iterable);
}
const evenNumbersIterator = createEvenNumbersIterator();
const result = collect( // 7. and collect the result
filter( // ⬆️ 6. keep only values higher than 1
val => val > 1, map( // ⬆️ 5. divide obtained values by 2
val => val / 2, take( // ⬆️ 4. take only six of them
6, cycle( // ⬆️ 3. make an infinite cycling sequence of them
take( // ⬆️ 2. take just three of them
3, evenNumbersIterator // ⬆️ 1. infinite sequence of even numbers
)
)
)
)
)
);
console.log(result);
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
- 78.
- 79.
- 80.
- 81.
- 82.
- 83.
- 84.
- 85.
- 86.
- 87.
- 88.
- 89.
- 90.
- 91.
- 92.
- 93.
- 94.
- 95.
- 96.
- 97.
- 98.
- 99.
- 100.
- 101.
- 102.
- 103.
- 104.
- 105.
- 106.
- 107.
- 108.
- 109.
- 110.
- 111.
- 112.
- 113.
- 114.
这是一大堆代码,很快我们将看到如何使用生成器和函数式编程概念来重构所有这些内容。保持关注,并注意我的后续文章,我们仍然有很多内容要讲。
作者:MelkorNemesis 译者:前端小智 来源:medium
原文:https://medium.com/@MelrNemesis/javascript-lazy-evaluation-iterables-iterators-e0770a5de96f
本文转载自微信公众号「 大迁世界」,可以通过以下二维码关注。转载本文请联系 大迁世界公众号。