论坛首页 Web前端技术论坛

JavaScript的Closure陷阱

浏览 4427 次
精华帖 (0) :: 良好帖 (0) :: 新手帖 (0) :: 隐藏帖 (0)
作者 正文
   发表时间:2007-08-17  
有这样一种新的JS pattern:Lazy Function Definition Pattern,realazy同志的翻译在此:http://realazy.org/blog/2007/08/16/lazy-function-definition-pattern/

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

  return f();
}


但是这个pattern真的很好吗?

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

  f._cache = result;
  return result;
}


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

我给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.
   发表时间:2007-08-17  
hax,这段代码
var foo = function() {
    var t = new Date();
    foo = function() {
        return t;
    };
    return foo();
};
应该等同于这样的吧
var foo = function() {
    var t = new Date();
    return function() {
      return t;
    }
}();

0 请登录后投票
   发表时间:2007-08-18  
roger 写道
hax,这段代码
var foo = function() {
    var t = new Date();
    foo = function() {
        return t;
    };
    return foo();
};
应该等同于这样的吧
var foo = function() {
    var t = new Date();
    return function() {
      return t;
    }
}();



不等同的。
上面那个直到被调用时候才new date
而下面那个,定义的时候就new date了
0 请登录后投票
   发表时间:2007-08-18  
不等的。你第二段代码已经执行了一次。
0 请登录后投票
   发表时间:2007-09-28  
已经执行了一次,能否详细说明一下呢,是否有在我看来只是多一个()
0 请登录后投票
   发表时间:2007-09-29  
前面已经说的很清楚了。第二个效果上差不多等价于第一个代码后再加一句foo()调用。
0 请登录后投票
   发表时间:2008-05-03  
偶觉得惰性函数定义模式是我们解决某些问题的一种有效方法,
万金油这种东西貌似并不存在。
我们需要的是在适合的时候使用他。
我就发现惰性函数定义模式的一个有效使用例子:
1,我们要的函数体需要根据环境(配置)的不同去执行相应的代码块
2,如果环境(配置)固定,代码块总是固定的。
那么我说惰性函数定义模式在这种需求下的意义就是提高代码的效率,提高多少和提高有没有意义我们不讨论,因为这个要和实际需求讨论,
那我就举一个例子:
本地化字符串替换,(当然我也觉得这个例子不够复杂不是很合适,但足够说明问题)。
本地化基本上是只要浏览器(环境配置)一运行就可以确定的了。
常见的本地化结构类似下面的语法
var I18N=function(s){
  if (localstring && localstring[locallang])
      return localstring[locallang][s] || s;
  return s;
}

如果用惰性函数定义模式的话类似这样:
var I18N=function(s){
  if (localstring && localstring[locallang])
      return function(s){localstring[locallang][s] || s;};
  return function(s){ return s;};
}

代码是随意写的,也许根据实际情况,还可以优化。
我想这仅仅是惰性函数定义模式的一个应用例子。其他的例子应该还有。
0 请登录后投票
   发表时间:2008-05-03  
你举的例子并不是所谓惰性定义。惰性定义的本质就是函数第一次执行的时候改写函数自身。

其实你仔细看过我的分析就知道了。所谓惰性定义,其出发点就是高效,但是我的分析已经阐明了,所谓高效是根本不存在的。

而且惰性定义在编程时是存在很大的潜在问题的,请看这篇分析

摘录一段:
引用

对于function的不变性,绝大多数用户会有不自觉的隐含认可。因为function调用的意义是明确的,不变的。(有明确说我这个函数就是经常变化的例子么?)基于此,用户不会预期函数对象本身的变化。

注意,用户可以接受初始化这样一件事情。这是很正常的。但是用户为什么要接受,第一次调用与以后调用的差别呢!所以问题就在这里。调用函数是一件 trivial的事情,不应该强迫用户了解调用之间的差别,这与用户的目标毫无关系。况且你就是告诉用户第一次调用和以后调用的差别,也不代表他们能理解这种副作用对于他们代码的影响。因此出现我说的那种情况的话,用户就会陷入迷茫。
0 请登录后投票
论坛首页 Web前端技术版

跳转论坛:
Global site tag (gtag.js) - Google Analytics