建议65:比较函数的惰性求值与非惰性求值
在JavaScript中,使用函数式风格编程时,应该对于表达式有着深刻的理解,并能够主动使用表达式的连续运算来组织代码。
1)在运算元中,除了JavaScript默认的数据类型外,函数也作为一个重要的运算元参与运算。
2)在运算符中,除了JavaScript的大量预定义运算符外,函数还作为一个重要的运算符进行计算和组织代码。
函数作为运算符参与运算,具有非惰性求值特性。非惰性求值行为自然会对整个程序产生一定的负面影响。先看下面这个示例:
var a = 2;
function f(x){
return x;
}
alert(f(a,a=a*a)); //2
alert(f(a)); //4
在上面的示例中,两次调用同一个函数并传递同一个变量,所返回的值却不一样。在第一次调用函数时,向其传递了两个参数,第二个参数是一个表达式,该表达式对变量a进行重新计算和赋值。也就是说,当调用函数时,第二个参数虽然不使用,但是也被计算了。这就是JavaScript的非惰性求值特性,也就是说,不管表达式是否被利用,只要在执行代码行中都会被计算。
如果在一个函数参数中无意添加了几个表达式,虽然这样不会对函数的运算结果产生影响,但是由于表达式被执行,就会对整个程序产生潜在的负面影响。
在惰性求值语言中,如果参数不被调用,那么无论参数是直接量还是某个表达式,都不会占用系统资源。但是,由于JavaScript支持非惰性求值,问题就变得很特殊了。
function f(){}
f( function(){while(true);}())
在上面的示例中,虽然函数f没有参数,但是在调用时将会执行传递给它的参数表达式,该表达式是一个死循环结构的函数值,最终将导致系统崩溃。
惰性函数模式是一种将对函数或请求的处理延迟到真正需要结果时进行的通用概念,很多应用程序都采用了这种概念。从惰性编程的角度来思考问题,可以帮助消除代码中不必要的计算。例如,在Scheme语言中,delay特殊表单接收一个代码块,它不会立即执行这个代码块,而是将代码和参数作为一个promise存储起来。如果需要promise产生一个值,就会运行这段代码。promise 随后会保存结果,这样将来再请求这个值时,该值就可以立即返回,而不用再次执行代码。这种设计模式在JavaScript中大有用处,尤其是在编写跨浏览器的、高效运行的库时非常有用。例如,下面是一个时间对象实例化的函数。
var t;
function f(){
t = t ? t : new Date();
return t;
}
f(); // 调用函数
上面的示例使用全局变量t来存储时间对象,这样在每次调用函数时都必须进行重新求值,代码的效率没有得到优化,同时全局变量t很容易被所有代码访问和操作,存在安全隐患。当然,可以使用闭包隐藏全局变量t,只允许在函数f内访问。
var f =(function(){
var t;
return function(){
t = t ? t : new Date();
return t;
}
})();
f();
这仍然没有提高调用时的效率,因为每次调用f依然需要求值:
var f = function() {
var t = new Date();
f = function() {
return t;
}
return f();
};
f();
在上面的示例中,函数f的首次调用将实例化一个新的Date对象并重置f到一个新的函数上,f在其闭包内包含Date对象。在首次调用结束之前,f的新函数值也已被调用并提供返回值。
函数f的调用都只会简单地返回t保留在其闭包内的值,这样执行起来非常高效。弄清这种模式的另一种途径是,外部函数f的首次调用是一个保证(promise),它保证了首次调用会重定义f为一个非常有用的函数,保证来自于Scheme的惰性求值机制。