求值策略所要解决的问题是:何时求值。之所以要控制求值的时间,往往是出于对内存占用和执行效率的考虑。在维基百科的求值策略词条中,列出了五种求值策略:

  • 预先求值,表达式绑定到变量时,立即求值并附加给变量
  • 延迟求值,表达式绑定到变量后,并不求值,直到变量被调用时才求值
  • 局部求值,又称柯里化
  • 分布求值,map/reduce,典型应用是分布式计算
  • 短路求值,与 (&&) 、或 (||) 逻辑运算

顾名思义,预先求值和延迟求值是一对对立的求值策略。在下面的 js 代码段中,声明了变量 x,x 赋值后又被调用了三次:

// 1
var x = 5 + 3 * (1 + 5 ^ 2);
// 2
console.log(x);
console.log(x + 2);
// 3
func(x);

在 1 处,变量 x 完成了声明和赋值等初始化工作,它的值由表达式 (5 + 3 * (1 + 5 ^ 2)) 决定。在 2 处,console.log() 函数调用了变量 x 两次。让我们暂时忽略 js 编译器的求值策略,从个人主观的理性思维来思考一下:面对预先求值和延迟求值,你会怎么选择呢?

做出选择之前,我们需要对变量 x 有一个复杂度的认知:如果表达式的复杂度高,那么该表达式所占用的内存空间也就越高,在变量 x 和表达式解绑前这段内存空间都无法释放掉,意味着 x 的间接内存占用了越高,此外,高复杂度也意味着较长的执行时间,所以复杂度和内存占用、执行效率至少是一种线性相关。

如此说来,那么我们应该选择预先求值的求值策略,这样的好处是:变量 x 初始化时表达式立即计算,x 被重新赋值为 83,重新赋值后表达式所占用的内存空间被释放掉,达到了节省内存空间的目的;多次调用变量 x 时,x 的值已经是可以直接用于运算的数值,而不是需要计算的表达式,减少了重复运算,提高了执行效率。

如果事情按照上面描述的美好愿景发展的话,就没有延迟求值策略出现的必要了。在 3 处,我们向函数 func() 传递了变量 x。这里的 func() 类似于一个黑盒,我们不了解其内部的处理机制,也无法确定 x 是否会被使用。这个时候延迟求值的价值就体现出来了,如果变量 x 在函数内部没有被使用,就不会执行求值,避免了预先求值所要执行的求值运算。

随着表达式的复杂度逐渐提高,预先求值和延迟求值在内存占用、执行效率上的差异就会愈发明显。所以根据数据类型的复杂度,js 将数据类型分为原始值和引用值,传递参数时,原始值按值传递,引用值按引用传递。

须知参差多态乃是幸福本源。