理解惰性求值
惰性求值,又叫做惰性计算,与其相反的是及早求值。
大多数的编程语言默认的是及早求值。
于下例代码:
a = b + c
print a
- 及早求值:add(b, c) 当即放回一个值,赋值给
a,在第二行打印a的值。 - 惰性求值:add(b, c) 过程被赋值给
a,而不是返回值,在第二行需要a的时候执行其存储的过程打印返回值。
惰性求值在处理大量数据的时候,可以在性能和可读性上取得极好平衡。
用 lodash 示例:
_(_.range(10000))
.map(i => console.log(i))
.take(2).value()
代码运行时,只打印了两行,take 和 map 的顺序没有影响结果。map 和 take 都没有被立即执行,直到 value() 这种出口才被执行。
与之相反的是 js 自带的 map():
_.range(10000)
.map(i => console.log(i))
.slice(0, 2)
程序在 map() 的时候便完全执行了,打印了整整 1 万行。而事实上,绝大多数的数据是没有必要处理的,因为被 slice() 丢弃掉了。
惰性求值性能比及早求值好?
不一定。论性能肯定是汇编语言最好了,它是及早求值的。
看下面一个例子:
const data = _(_.range(4)).map(i => console.log(i))
data.take(3).value()
data.take(2).value()
上面这段代码输出了 5 行(比及早求值的方法输出 4 行还多 1 行),因为有重复的过程被执行了。
用 lodash 的时候要注意到链式调用的分叉,自行判断一下是否先用 value() 完成过程进行求值比较好。
我个人经验是,绝大多数时候,处理的数据必定是来自读写接口,去向读写接口。得益于惰性计算,不必一次将所有数据写入内存,而是依个处理的,像流一样。那么就算是重复了一些过程,也省下了内存,性能上孰优孰劣就不好说了。
const files = _(paths).map(p => fs.readFileSync(p)) // 此时没有读取文件
files.filter(isTypeA)
.map(handlerA)
.forEach(f => fs.writeFileSync(...)) // forEach 是出口,此时文件才被读取
files.filter(isTypeB)
.map(handlerB)
.forEach(f => fs.writeFileSync(...))
以上代码中,假定typeA 和 typeB 存在交集,即会有重复读取交集中的文件,但是其读进并处理后就立即被写出了。
若是写成及早求值的方式,在 paths.map() 中所有文件被读取到内存中,就可能造成内存不够。所以只能使用非函数式的循环判断,而这样嵌套的代码确实不如链式调用好阅读(当代码量上去的时候就更容易观察到):
for (const p of paths) {
const f = fs.readFileSync(p)
if(isTypeA(f)) {
const result = handlerA(f)
fs.writeSync(result)
}
if(isTypeB(f)) {
const result = handlerB(f)
fs.writeSync(result)
}
})