JavaScript的Closure陷阱

有这样一种新的JS pattern:[url=http://peter.michaux.ca/article/3556/]Lazy Function Definition Pattern[/url],realazy同志的翻译在此:[url]http://realazy.org/blog/2007/08/16/lazy-function-definition-pattern/[/url]

大体上,这个pattern就是在函数的第一次运行时重新定义自身,代码示意如下:
[code]
var f = function () {
... // calculation
if (condition == 1) {
f = function () {...}
} else if (condition == 2) {
f = function () {...}
} else ...

return f();
}
[/code]

但是这个pattern真的很好吗?

原文中有若干个用作对照的方法,其中方法三(Solution #3)的意思如下:
[code]
function f() {
if (_cache in f) return f._cache;

... // calculation
var result = ...
...

f._cache = result;
return result;
}
[/code]

其他对照方法,可以看原文内容。

我给realazy同志的回复摘录如下:

realazy同志,吹捧的话我就不说了。俺说点不中听的。

我认为这一技巧在绝大多数场合是没有什么特别优点的,相反存在着陷阱。

在目前所有的js引擎中,一次判断的性能开销与其他开销(例如一次函数调用的开销)相比,根本不在一个数量级上。而你所说的”如果之前那些条件非常多和复杂的话”,这个假设是错误的,因为并不需要每次都进行非常多和复杂的条件判断——方法三本质上是一个cache模式。如果这些条件逻辑上每次都需要重新计算,那也不可能去使用你的“函数重新定义自身”的方法。

注意,对于方法三的运用来说,你前面的示例和后面的getScrollY其实有一定的不同。前者是直接缓存函数的运算结果,后者则需要缓存函数分支条件。相对来说后者看上去更适合用你的”函数重新定义自身”的方法,而前者则相反。

在”缓存运算结果”的case里,方法四的实际效率比方法三更低。我们来分析一下:

第一次调用时,方法三需要一次boolean判断,然后运算,然后缓存结果(一次对local的赋值),然后返回结果。方法四需要运算,然后产生一个闭包,重新定义自己(一次对外层scope的赋值),然后再调用一次自身,返回调用结果。

可见,第一次调用的时候,肯定是方法三快。

再看以后的调用,方法三需要一次boolean判断,然后返回缓存结果。方法四直接返回结果。

看上去方法四会快,但是注意,方法三的缓存结果是在local上,而方法四,是在外面一层的scope上,因此变量搜索上,理论上方法三会比方法四快一点。当然变量搜索的开销其实是很小的,但反过来,boolean判断的开销也是非常小的。因此很难说方法四就比方法三快,即使快,也快得非常有限,可以忽略不计。

但是方法四,其实存在另一方面的问题。

在方法三中,如果有很多的中间运算结果,在第一次运行后,这些结果都会被自动回收回去。而方法四中,由于替换自身的闭包对于被替换的方法内的所有变量都存在引用,因此这些变量所指向的对象全部不会被回收回去!!除非你手动把变量设置为null——手动清空显然不是什么好模式。

当然理论上,在闭包中没有明确用到的外部变量其实可以不影响垃圾回收(除非在闭包中存在eval之类的东西),但是目前我已知的主流js引擎似乎都没有做这方面的优化。我猜测,也许有些编译型的js引擎,会作此类优化。例如rhino的编译成java class的功能,以及ActionScript。有兴趣的同志可以测试一下。

就目前来说,不恰当的使用方法四,很可能会造成内存泄漏。即使在getScrollY的例子中,根本不存在任何局部变量,其所产生的闭包中也不存在任何原本方法的可能的引用,但实际上原方法仍是不会被回收的。如果你要测试的话,可以在第一次调用前执行getScrollY.wasteMemory = new Array(100000).join(’foobar’);然后观察浏览器的内存在第一次调用后是否就减小了。我虽然没有实测过,但是根据经验,结果应该是不会释放这些内存的。而且在第一次调用之后,你甚至再也没有机会去释放这个内存,呵呵。

值得注意的是,这并非浏览器bug造成的内存泄漏,而只是由于缺乏优化而造成的泄漏。

所以你的这个模式恐怕既不健壮,也不高效,至于说是不是紧凑呢,我自己是没看出来方法四比方法三紧凑在哪里。而且就这个模式本身来说,并没有用到很functional的技巧。

BTW,我看到最后发现是翻译的。。。不知道你有没有兴趣把我的回复翻译回去贴到原出处去,呵呵。

----
以上是给realazy的回复。

实际上,我这个文章里说的内存泄漏在任何closure中都有可能存在的。这个就是closure的陷阱了。

根据Brenden Eich的说法,closure的信息隐藏所模拟的private访问,比普通的对象访问慢三倍。而closure的内存消耗比一般对象方法要大三倍。他怎么计算的我不清楚,不过这恐怕就是为什么ES4要加入类的原因之一。

我猜想另外一个原因就是closure实际上内含的复杂性,和潜在的内存管理问题。

对于有eval特性的语言来说,有eval调用,理论上就可能访问外层的任何变量(除非eval的参数可以被推导出是常量),因此不能自动释放掉任何对象。而除去eval是否就能释放呢?js还有一个讨厌的with,如果with的值不能被确定为常量,则被with包裹的那一块,就不能static的决定变量实际上是指哪一个!因此它减少了优化的可能。ES4的讨论中甚至有人因此希望改变with的定义。但是从向前兼容ES3的原则来说,这是不可行的(比如PIES的下一个版本就依赖于with的这个让人既爱又恨的特性,呵呵)。

这个问题在jindw的JSA压缩内部变量的时候也有影响。所以jindw怒斥dojo的压缩器存在问题,好像也是由于这些问题引起的。

我说的问题,确实可以通过引擎优化解决,但是这只是理想中的优化,ecma规范并不能推导出这个要求。而目前为止,似乎没有一个js引擎做这个优化。。。

That's the real world.
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值