`
hax
  • 浏览: 965154 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

JavaScript的Closure陷阱

    博客分类:
  • JS
阅读更多
有这样一种新的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.
分享到:
评论
7 楼 hax 2008-05-03  
你举的例子并不是所谓惰性定义。惰性定义的本质就是函数第一次执行的时候改写函数自身。

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

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

摘录一段:
引用

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

注意,用户可以接受初始化这样一件事情。这是很正常的。但是用户为什么要接受,第一次调用与以后调用的差别呢!所以问题就在这里。调用函数是一件 trivial的事情,不应该强迫用户了解调用之间的差别,这与用户的目标毫无关系。况且你就是告诉用户第一次调用和以后调用的差别,也不代表他们能理解这种副作用对于他们代码的影响。因此出现我说的那种情况的话,用户就会陷入迷茫。
6 楼 achun 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;};
}

代码是随意写的,也许根据实际情况,还可以优化。
我想这仅仅是惰性函数定义模式的一个应用例子。其他的例子应该还有。
5 楼 hax 2007-09-29  
前面已经说的很清楚了。第二个效果上差不多等价于第一个代码后再加一句foo()调用。
4 楼 cqhydz 2007-09-28  
已经执行了一次,能否详细说明一下呢,是否有在我看来只是多一个()
3 楼 hax 2007-08-18  
不等的。你第二段代码已经执行了一次。
2 楼 i_love_sc 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了
1 楼 roger 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;
    }
}();

相关推荐

    closure-compiler,javascript检查器和优化器。.zip

    闭包编译器是一个让javascript下载和运行更快的工具。它是一个真正的javascript编译器。它不是从源语言编译成机器代码,而是从javascript编译成更好的...它还检查语法、变量引用和类型,并警告常见的javascript陷阱。

    JavaScript闭包(closure).pdf

    JavaScript中的闭包是一种高级特性,它是...在实际开发中,合理利用闭包可以提高代码的复用性和可维护性,同时也能避免一些常见的编程陷阱。通过深入学习和实践,可以更好地运用闭包这一强大的工具来解决复杂的问题。

    closure-compiler:围绕 Google Closure Compiler 的 PHP Wrapper

    它还检查语法、变量引用和类型,并对常见的 JavaScript 陷阱发出警告。 为什么这个库存在? Google 提供的 Closure Compiler (CC) 可作为 Web 应用程序使用,也可作为命令行 Java jarfile 下载。 此文件可用于运行...

    最容易犯的JavaScript错误.doc

    或者使用`let`关键字(ES6引入)来创建块级作用域,避免闭包陷阱: ```javascript var elements = document.getElementsByTagName('tagname'); for (let i = 0; i ; i++) { elements[i].onclick = function() {...

    Ajax World (Javascript)

    综上所述,Ajax World与JavaScript紧密相关,它们共同推动了Web应用的交互性和实时性,但同时也需要开发者理解和避免JavaScript的一些陷阱,充分利用其强大的功能,如闭包和异步通信,来构建高性能的Web应用。

    Google JavaScript 编码规范指南

    - **代码优化**:利用现代JavaScript编译工具,如Babel、Closure Compiler等,进行代码压缩和优化,提升运行效率。 #### 五、结论 Google JavaScript编码规范指南不仅是Google内部项目的技术标准,也是广大...

    javascript 深入编程网页收集(超级经典)

    **闭包(Closure)** 闭包是JavaScript中的一个高级特性,它允许函数访问并操作外部作用域的变量,即使在其外部作用域已经被销毁后。闭包是由函数和与其相关的引用环境组合而成的实体,也就是说,一个函数能够记住...

    js-pitfall-examples:常见的 JavaScript 陷阱示例

    JavaScript,作为世界上最受欢迎的编程语言之一,经常会给开发者带来一些陷阱和误区,这些陷阱可能导致程序出乎意料的行为。在“js-pitfall-examples”这个项目中,我们收集了一些常见的JavaScript陷阱,通过实例来...

    javascript闭包高级教程

    ### JavaScript闭包高级教程 #### 简介 在JavaScript编程中,“闭包”是一个非常重要的概念,尤其对于希望深入理解和高效使用...正确理解和使用闭包不仅能帮助开发者编写更优雅的代码,还能避免潜在的性能陷阱。

    JavaScript10分钟速成(js-in-ten-minutes).pdf

    JavaScript 速成指南 JavaScript 是一种广泛使用的脚本语言,广泛应用于 web 开发、移动应用开发和桌面应用开发等领域。...但是,需要注意 JavaScript 中的一些坑和陷阱,以避免编码错误和安全问题。

    闭合编译器:JavaScript检查器和优化器

    它还会检查语法,变量引用和类型,并警告常见JavaScript陷阱。 入门 安装编译器的最简单方法是使用或 : yarn global add google-closure-compiler # OR npm i -g google-closure-compiler 软件包管理器将为您...

    javascript中一些数据类型以及奇怪的特性

    然而,JavaScript的数据类型并不像其他一些编程语言那样简单,它有一些独特的特性和陷阱。下面我们将深入探讨JavaScript中的数据类型及其一些奇怪的特性。 1. **基本数据类型** - **Undefined**:未定义的值,当...

    javascript闭包真经

    根据提供的文件信息,本文将围绕“JavaScript闭包”这一核心概念进行深入解析,并结合描述中的资源分享链接,进一步探讨闭包在JavaScript编程中的应用、优势及其潜在陷阱。 ### JavaScript闭包概述 #### 1. 闭包...

    javascript作用域链(Scope Chain)用法实例解析

    正确理解和运用作用域链可以避免许多常见的编程陷阱,并帮助你编写更高效、更易于维护的代码。在实际开发中,掌握作用域链的原理不仅有助于提升代码质量,还能更好地利用 JavaScript 的特性,如异步编程、事件处理和...

    JavaScript静态作用域和动态作用域实例详解

    JavaScript的静态作用域还体现在闭包(closure)上,闭包允许函数访问并操作其定义时的作用域,即使该作用域在其执行时已经不存在。这是JavaScript实现模块化和数据封装的重要机制。 总之,JavaScript采用静态作用...

    JAVASCRIPT函数作用域和提前声明 分享

    理解这些基本概念对于避免常见的JavaScript陷阱至关重要,例如变量覆盖和意外的全局变量。在编写JavaScript代码时,应尽量使用`let`和`const`来替代`var`,因为ES6引入的`let`和`const`提供了块级作用域,有助于减少...

    理解JavaScript中的闭包

    它将深入探讨闭包的工作原理,提供实例来解释如何在实际项目中应用闭包,以及如何避免常见的闭包陷阱。通过学习这个主题,开发者可以提升JavaScript编程技能,编写出更健壮、高效的代码,适应Web开发的各种需求。

    so和such用法小结.doc

    5. 闭包的陷阱与内存问题 由于闭包会保持对外部作用域的引用,可能导致内存泄漏。如果闭包引用的外部变量不再需要,但闭包仍然存在,这些变量将无法被垃圾回收器回收。 6. 闭包与this指向 闭包不会改变this的指向...

Global site tag (gtag.js) - Google Analytics