`
abruzzi
  • 浏览: 449290 次
  • 性别: Icon_minigender_1
  • 来自: 西安
社区版块
存档分类
最新评论

JavaScript内核系列 第7章 闭包

阅读更多

第七章 闭包

闭包向来给包括JavaScript程序员在内的程序员以神秘,高深的感觉,事实上,闭包的概念在函数式编程语言中算不上是难以理解的知识。如果对作用域,函数为独立的对象这样的基本概念理解较好的话,理解闭包的概念并在实际的编程实践中应用则颇有水到渠成之感。

DOM的事件处理方面,大多数程序员甚至自己已经在使用闭包了而不自知,在这种情况下,对于浏览器中内嵌的JavaScript引擎的bug可能造成内存泄漏这一问题姑且不论,就是程序员自己调试也常常会一头雾水。

用简单的语句来描述JavaScript中的闭包的概念:由于JavaScript中,函数是对象,对象是属性的集合,而属性的值又可以是对象,则在函数内定义函数成为理所当然,如果在函数func内部声明函数inner,然后在函数外部调用inner,这个过程即产生了一个闭包。

7.1闭包的特性

我们先来看一个例子,如果不了解JavaScript的特性,很难找到原因:

 

var outter = [];
function clouseTest () {
    var array = ["one", "two", "three", "four"];
    for(var i = 0; i < array.length;i++){
       var x = {};
       x.no = i;
       x.text = array[i];
       x.invoke = function(){
           print(i);
       }
       outter.push(x);
    }
}
 
//调用这个函数
clouseTest();
 
print(outter[0].invoke());
print(outter[1].invoke());
print(outter[2].invoke());
print(outter[3].invoke());

 

运行的结果如何呢?很多初学者可能会得出这样的答案:


0
1
2
3

 

然而,运行这个程序,得到的结果为:

4
4
4
4

 

其实,在每次迭代的时候,这样的语句x.invoke = function(){print(i);}并没有被执行,只是构建了一个函数体为”print(i);”的函数对象,如此而已。而当i=4时,迭代停止,外部函数返回,当再去调用outter[0].invoke()时,i的值依旧为4,因此outter数组中的每一个元素的invoke都返回i的值:4

 

如何解决这一问题呢?我们可以声明一个匿名函数,并立即执行它:

 

var outter = [];
 
function clouseTest2(){
    var array = ["one", "two", "three", "four"];
    for(var i = 0; i < array.length;i++){
       var x = {};
       x.no = i;
       x.text = array[i];
       x.invoke = function(no){
           return function(){
              print(no);
           }
       }(i);
       outter.push(x);
    }  
}
 
clouseTest2();

 

这个例子中,我们为x.invoke赋值的时候,先运行一个可以返回一个函数的函数,然后立即执行之,这样,x.invoke的每一次迭代器时相当与执行这样的语句:

 

//x == 0
x.invoke = function(){print(0);}
//x == 1
x.invoke = function(){print(1);}
//x == 2
x.invoke = function(){print(2);}
//x == 3
x.invoke = function(){print(3);}

 

 

这样就可以得到正确结果了。闭包允许你引用存在于外部函数中的变量。然而,它并不是使用该变量创建时的值,相反,它使用外部函数中该变量最后的值。

7.2闭包的用途

现在,闭包的概念已经清晰了,我们来看看闭包的用途。事实上,通过使用闭包,我们可以做很多事情。比如模拟面向对象的代码风格;更优雅,更简洁的表达出代码;在某些方面提升代码的执行效率。

7.2.1 匿名自执行函数

上一节中的例子,事实上就是闭包的一种用途,根据前面讲到的内容可知,所有的变量,如果不加上var关键字,则默认的会添加到全局对象的属性上去,这样的临时变量加入全局对象有很多坏处,比如:别的函数可能误用这些变量;造成全局对象过于庞大,影响访问速度(因为变量的取值是需要从原型链上遍历的)。除了每次使用变量都是用var关键字外,我们在实际情况下经常遇到这样一种情况,即有的函数只需要执行一次,其内部变量无需维护,比如UI的初始化,那么我们可以使用闭包:

 

var datamodel = {
    table : [],
    tree : {}
};
 
(function(dm){
    for(var i = 0; i < dm.table.rows; i++){
       var row = dm.table.rows[i];
       for(var j = 0; j < row.cells; i++){
           drawCell(i, j);
       }
    }
   
    //build dm.tree  
})(datamodel);

 

我们创建了一个匿名的函数,并立即执行它,由于外部无法引用它内部的变量,因此在执行完后很快就会被释放,关键是这种机制不会污染全局对象。

7.2.2缓存

再来看一个例子,设想我们有一个处理过程很耗时的函数对象,每次调用都会花费很长时间,那么我们就需要将计算出来的值存储起来,当调用这个函数的时候,首先在缓存中查找,如果找不到,则进行计算,然后更新缓存并返回值,如果找到了,直接返回查找到的值即可。闭包正是可以做到这一点,因为它不会释放外部的引用,从而函数内部的值可以得以保留。

 

var CachedSearchBox = (function(){
    var cache = {},
       count = [];
    return {
       attachSearchBox : function(dsid){
           if(dsid in cache){//如果结果在缓存中
              return cache[dsid];//直接返回缓存中的对象
           }
           var fsb = new uikit.webctrl.SearchBox(dsid);//新建
           cache[dsid] = fsb;//更新缓存
           if(count.length > 100){//保正缓存的大小<=100
              delete cache[count.shift()];
           }
           return fsb;      
       },
 
       clearSearchBox : function(dsid){
           if(dsid in cache){
              cache[dsid].clearSelection();  
           }
       }
    };
})();
 
CachedSearchBox.attachSearchBox("input1");
 

 

这样,当我们第二次调用CachedSearchBox.attachSerachBox(“input1”)的时候,我们就可以从缓存中取道该对象,而不用再去创建一个新的searchbox对象。

7.2.3 实现封装

可以先来看一个关于封装的例子,在person之外的地方无法访问其内部的变量,而通过提供闭包的形式来访问:

 

var person = function(){
    //变量作用域为函数内部,外部无法访问
    var name = "default";   
   
    return {
       getName : function(){
           return name;
       },
       setName : function(newName){
           name = newName;
       }
    }
}();
 
print(person.name);//直接访问,结果为undefined
print(person.getName());
person.setName("abruzzi");
print(person.getName());

 

得到结果如下:

 

undefined
default
abruzzi

 

         闭包的另一个重要用途是实现面向对象中的对象,传统的对象语言都提供类的模板机制,这样不同的对象(类的实例)拥有独立的成员及状态,互不干涉。虽然JavaScript中没有类这样的机制,但是通过使用闭包,我们可以模拟出这样的机制。还是以上边的例子来讲:

 

function Person(){
    var name = "default";   
   
    return {
       getName : function(){
           return name;
       },
       setName : function(newName){
           name = newName;
       }
    }
};
 
 
var john = Person();
print(john.getName());
john.setName("john");
print(john.getName());
 
var jack = Person();
print(jack.getName());
jack.setName("jack");
print(jack.getName());

 

运行结果如下:

 

default
john
default
jack

 

由此代码可知johnjack都可以称为是Person这个类的实例,因为这两个实例对name这个成员的访问是独立的,互不影响的

事实上,在函数式的程序设计中,会大量的用到闭包,我们将在第八章讨论函数式编程,在那里我们会再次探讨闭包的作用。

7.3应该注意的问题

7.3.1内存泄漏

在不同的JavaScript解释器实现中,由于解释器本身的缺陷,使用闭包可能造成内存泄漏,内存泄漏是比较严重的问题,会严重影响浏览器的响应速度,降低用户体验,甚至会造成浏览器无响应等现象。

JavaScript的解释器都具备垃圾回收机制,一般采用的是引用计数的形式,如果一个对象的引用计数为零,则垃圾回收机制会将其回收,这个过程是自动的。但是,有了闭包的概念之后,这个过程就变得复杂起来了,在闭包中,因为局部的变量可能在将来的某些时刻需要被使用,因此垃圾回收机制不会处理这些被外部引用到的局部变量,而如果出现循环引用,即对象A引用BB引用C,而C又引用到A,这样的情况使得垃圾回收机制得出其引用计数不为零的结论,从而造成内存泄漏。

7.3.2上下文的引用

关于this我们之前已经做过讨论,它表示对调用对象的引用,而在闭包中,最容易出现错误的地方是误用了this。在前端JavaScript开发中,一个常见的错误是错将this类比为其他的外部局部变量:

 

$(function(){
    var con = $("div#panel");
    this.id = "content";
    con.click(function(){
       alert(this.id);//panel
    });
});

 

此处的alert(this.id)到底引用着什么值呢?很多开发者可能会根据闭包的概念,做出错误的判断:


content

 

 

理由是,this.id显示的被赋值为content,而在click回调中,形成的闭包会引用到this.id,因此返回值为content。然而事实上,这个alert会弹出”panel”,究其原因,就是此处的this,虽然闭包可以引用局部变量,但是涉及到this的时候,情况就有些微妙了,因为调用对象的存在,使得当闭包被调用时(当这个panelclick事件发生时),此处的this引用的是con这个jQuery对象。而匿名函数中的this.id = “content”是对匿名函数本身做的操作。两个this引用的并非同一个对象。

         如果想要在事件处理函数中访问这个值,我们必须做一些改变:

 

$(function(){
    var con = $("div#panel");
    this.id = "content";
    var self = this;
    con.click(function(){
       alert(self.id);//content
    });
});

 

 

这样,我们在事件处理函数中保存的是外部的一个局部变量self的引用,而并非this。这种技巧在实际应用中多有应用,我们在后边的章节里进行详细讨论。关于闭包的更多内容,我们将在第九章详细讨论,包括讨论其他命令式语言中的“闭包”,闭包在实际项目中的应用等等。

 

附:由于作者本身水平有限,文中难免有纰漏错误等,或者语言本身有不妥当之处,欢迎及时指正,提出建议,参与讨论,谢谢大家!

分享到:
评论
18 楼 hsmsyy 2014-10-28  
这里应该是原创了吧,楼主我觉得闭包的作用:实现面向对象。有待商榷啊,javascript本身是可以面向对象的啊。
function Person(){ 
    var name = "default";    
    

       var getName = function(){ 
           return name; 
       }; 
        var setName = function(newName){ 
           name = newName; 
       } 

}; 
  
  
var john = new Person(); 
print(john.getName()); 
john.setName("john"); 
print(john.getName()); 
  
var jack = new Person(); 
print(jack.getName()); 
jack.setName("jack"); 
print(jack.getName());


这样写和闭包的写法是一样的吧,所以用闭包应该没有意义
17 楼 lixunhuanmarry 2012-09-05  
我当初自己琢磨了好久才想到用self来代替this,早看到你的书就好啦
16 楼 kmkim 2010-08-11  
恩,写的不错啊。。。。支持
15 楼 liushilang 2010-05-19  
<p>看了你写的,自己也照着写一个可实现缓存的东东。欢迎拍砖</p>
<pre name="code" class="缓存器">var adder = function(n){
var t = 10;
var org = n+"-"+Math.random();
return{
getinfo:function(){
document.writeln("\n\t"+org);
return n+t;
  }
   };
};

var CachedSearchBox = (function(){
var cache = {},count = [],totalCount = 20;

return {
attachSearchBox:function(dsid){
  if(dsid in cache){
print("old:::"+cache[dsid].getinfo());
return cache[dsid];
}
var fsb = new adder(dsid);
print("new::::"+fsb.getinfo());
cache[dsid] = fsb;
if(count.length&gt;=totalCount){    
var out = count.shift();
print("del::::::::::::"+out.getinfo())
delete cache[out];
}
count.push(fsb);
return fsb;
  },
   clearSearchBox:function(dsid){
if(dsid in cache){
//cache[dsid].clearSelection();
delete cache[dsid];
count.shift();
}
  },
  getcount:function(){//获取对象的长度
    print("array length:"+count.length);
return count.length;
  },
  setTotalCount:function(c){//设置缓存大小
totalCount = c;
  }
};
})();

CachedSearchBox.getcount();
print("&lt;br&gt;");

(function(){
   for(var i=0;i&lt;100;i++){
     CachedSearchBox.attachSearchBox(Math.ceil(Math.random()*120))
  print("~~~~~");
  CachedSearchBox.getcount();
      print("&lt;br&gt;");
  if(i==30)
CachedSearchBox.setTotalCount(40);
   }
})();

function print(a1){
document.writeln(a1+"\n\t");
}</pre>
<p> </p>
14 楼 weiqingfei 2010-05-11  
yyg1107 写道
有点不明白的,7.2.3中的2个例子的var person = function(){...}();var person = function(){...};有什么区别?好像就只是多了个小括号而已。


一个是变量声明,一个是匿名调用。
嗯,这儿起同样的名字确实让人混淆。
13 楼 yyg1107 2010-05-11  
有点不明白的,7.2.3中的2个例子的var person = function(){...}();var person = function(){...};有什么区别?好像就只是多了个小括号而已。
var person = function(){  
    //变量作用域为函数内部,外部无法访问  
    var name = "default";     
     
    return {  
       getName : function(){  
           return name;  
       },  
       setName : function(newName){  
           name = newName;  
       }  
    }  
}();  
   
print(person.name);//直接访问,结果为undefined  
print(person.getName());  
person.setName("abruzzi");  
print(person.getName());

var person = function(){
    var name = "default";     
     
    return {  
       getName : function(){  
           return name;  
       },  
       setName : function(newName){  
           name = newName;  
       }  
    }  
};     
   
var john = person();  
print(john.getName());  
john.setName("john");  
print(john.getName());  
   
var jack = person();  
print(jack.getName());  
jack.setName("jack");  
print(jack.getName()); 
12 楼 abruzzi 2010-05-06  
啊哦,马上要被投成新手帖了!本来打算将闭包放在函数式编程那一章的,但是有朋友建议独立出来,结果独立出来后内容显得有些单薄,呵呵。
11 楼 xiaobai233 2010-05-06  
非常易于理解
10 楼 abruzzi 2010-05-06  
第八章 面向对象的JavaScript http://www.iteye.com/topic/660049
9 楼 fisherhe 2010-05-06  
不错,看了深有体会
8 楼 chemzqm 2010-05-05  
根源在于调用对象和作用域链,理解了那些,闭包就很简单了
7 楼 abruzzi 2010-05-04  
寻找出路的苍蝇 写道
对闭包的理论讲解有些单薄吧?我觉得像是执行环境、作用域链这些概念还是有必要解释解释的,而不仅是给闭包下一个简单的定义。例如对7.1节的那个例子,可以从更深入的角度分析下引起这种现象的原因而不是直接给出分析的结果。
请LZ参考。


谢谢你的建议。
刚开始些这章的时候,考虑如果涉及一大堆的理论部分,难免看着很眩晕,毕竟闭包的概念本身并不复杂。加上之前关于执行环境,作用域都已经讲过了。但是只写成关于闭包的特点,用途等又有点单薄,不堪深入思考。我尽量再权衡一下,做些理论方面的补充吧。
6 楼 寻找出路的苍蝇 2010-05-04  
对闭包的理论讲解有些单薄吧?我觉得像是执行环境、作用域链这些概念还是有必要解释解释的,而不仅是给闭包下一个简单的定义。例如对7.1节的那个例子,可以从更深入的角度分析下引起这种现象的原因而不是直接给出分析的结果。
请LZ参考。
5 楼 abruzzi 2010-05-04  
λ-lambda 写道
例子确实挺好的,不过有点疑问:
7.2.1 匿名自执行函数 这一小节为什么会出现在这里?感觉跟闭包关系没有什么关系

不好意思,这个例子确实不应该在这里,因为没有一个全局变量hold这个匿名自执行函数的返回值(因为这个例子中,该函数执行完之后就释放了),想要说的应该是Cache那个例子,呵呵。

谢谢你提出的问题!
4 楼 λ-lambda 2010-05-04  
例子确实挺好的,不过有点疑问:
7.2.1 匿名自执行函数 这一小节为什么会出现在这里?感觉跟闭包关系没有什么关系
3 楼 abruzzi 2010-05-04  
childrentown 写道
关于JavaScript闭包的文章已经不胜其多,最好的那篇是从实现的角度来解释的,深入倒是深入,不过失之于理论化。这篇不错,例子比较多,也挺贴切的。最好是两个对照起来看,呵呵。

嗯,对照着看挺好。不过我尽量再在其中加入些理论的东西,不论是只有理论,还是只有实践,都太单薄,两者结合才好。
2 楼 childrentown 2010-05-04  
关于JavaScript闭包的文章已经不胜其多,最好的那篇是从实现的角度来解释的,深入倒是深入,不过失之于理论化。这篇不错,例子比较多,也挺贴切的。最好是两个对照起来看,呵呵。
1 楼 kingtoon 2010-05-04  
不错 继续关注

相关推荐

    python入门到高级全栈工程师培训 第3期 附课件代码

    第7章 01 ip地址与子网划分 02 ip地址配置 03 虚拟机网络模式 04 三层隔离验证试验 第8章 01 上节课复习 02 软件包介绍 03 rpm软件包管理 04 yum软件包管理 05 源码安装python3.5 06 ssh服务 07 apache服务 08 ...

    ActionScript开发技术大全

    第7章ActionScript3.0中的日期和时间 139 7.1日期与时间 139 7.1.1创建日期对象 139 7.1.2日期对象的属性与方法 140 7.1.3日期格式化 143 7.2时间间隔 144 7.2.1使用Timer类 144 7.2.2秒表示例 146 7.3小结 149 第8...

    面试大全新-148P.docx

    前端开发是现代互联网应用的核心组成部分,涉及到一系列的技术和概念。以下是一些重要的前端开发知识点,主要涵盖HTML、CSS、JavaScript、AJAX以及新兴框架如React、Vue和Angular等。 1. HTML篇: - Web标准理解:...

    performance-profiling:关于配置应用程序的FPS和内存占用量的步骤的简单演示

    在IT行业中,性能分析是优化应用程序的关键环节,尤其是在JavaScript领域,因为JavaScript是许多网页和Web应用的核心编程语言。本文将详细讲解如何配置应用程序以监控其帧率(FPS)和内存占用,以提升性能。 首先,...

    面试题总结.docx

    根据给定文件的信息,我们可以提炼出一系列与前端开发相关的知识点,包括但不限于技术概念、编码实践、面试技巧等。下面将围绕这些方面展开详细介绍。 ### 一、箭头函数与普通函数的区别 箭头函数和普通函数的主要...

    ajax验证码异步刷新源码新手java-front-end-face-questions:史上最全前端开发面试问题及答案整理

    数据类型、面向对象、继承、闭包、插件、作用域、跨域、原型链、模块化、自定义事件、内存泄漏、事件机制、异步装载回调、模板引擎、Nodejs、JSON、ajax等。 其他: HTTP、安全、正则、优化、重构、响应式、移动端、...

    程序员面试刷题的书哪个好-Interview-questions:分享自己整理的前端面试题及答案

    数据类型、面向对象、继承、闭包、插件、作用域、跨域、原型链、模块化、自定义事件、内存泄漏、事件机制、异步装载回调、模板引擎、Nodejs、JSON、ajax等。 其他: HTTP、安全、正则、优化、重构、响应式、移动端、...

    阿里前端面试第三期.pdf

    - parseInt接受两个参数,第一个是字符串,第二个是基数(radix)。 - 因此,['1','2','3'].map((item, index) =&gt; parseInt(item, index))中,parseInt将索引作为基数,导致解析结果不符合预期。 4. 防抖与节流:...

Global site tag (gtag.js) - Google Analytics