深入理解JavaScript闭包概念

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

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

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

闭包的特性
我们先来看一个例子,如果不了解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);}
这样就可以得到正确结果了。闭包允许你引用存在于外部函数中的变量。然而,它并不是使用该变量创建时的值,相反,它使用外部函数中该变量最后的值。

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

匿名自执行函数
上一节中的例子,事实上就是闭包的一种用途,根据前面讲到的内容可知,所有的变量,如果不加上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);
我们创建了一个匿名的函数,并立即执行它,由于外部无法引用它内部的变量,因此在执行完后很快就会被释放,关键是这种机制不会污染全局对象。

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

实现封装
可以先来看一个关于封装的例子,在person之外的地方无法访问其内部的变量,而通过提供闭包的形式来访问:
<strong>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());</strong>
得到结果如下:
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
由此代码可知,john和jack都可以称为是Person这个类的实例,因为这两个实例对name这个成员的访问是独立的,互不影响的。

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

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

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

上下文的引用
关于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的时候,情况就有些微妙了,因为调用对象的存在,使得当闭包被调用时(当这个panel的click事件发生时),此处的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。这种技巧在实际应用中多有应用,我们在后边的章节里进行详细讨论。关于闭包的更多内容,我们将在第九章详细讨论,包括讨论其他命令式语言中的“闭包”,闭包在实际项目中的应用等等。
...全文
8766 60 打赏 收藏 转发到动态 举报
写回复
用AI写文章
60 条回复
切换为时间正序
请发表友善的回复…
发表回复
hakunamatata_01 2016-05-13
  • 打赏
  • 举报
回复
学习~~~~~~
lihao_423 2016-05-07
  • 打赏
  • 举报
回复
受教了!js博大精深啊
人鱼传说 2015-11-16
  • 打赏
  • 举报
回复
楼主很吊,写得很好,闭包和this不能准确理解和运用的,的确不能说自已理解了javascript了
qq_27991461 2015-11-01
  • 打赏
  • 举报
回复
顶,不错,值得学习
qq_32436795 2015-10-31
  • 打赏
  • 举报
回复
我怎么调试呢,?可以帮帮忙不
aresnet 2014-03-18
  • 打赏
  • 举报
回复
谢楼主,学习了
6442b974c5bde03f 2014-03-17
  • 打赏
  • 举报
回复
长知识了,谢谢
楊松坤 2014-02-19
  • 打赏
  • 举报
回复
<‘▽′> 老虎 QQ:125004485
liwei3513866 2014-01-25
  • 打赏
  • 举报
回复
做了两年WEB开发,这种JS还写得真少
fnzh110 2014-01-25
  • 打赏
  • 举报
回复
挺复杂的,需要反复学习.
-Sykes 2014-01-25
  • 打赏
  • 举报
回复
C#里也有闭包,可以通过lambda表达式创建
-Sykes 2014-01-25
  • 打赏
  • 举报
回复
如果我理解的没错的话,闭包应该就是函数内部变量到了生命周期末而没有被回收,因为他被另外一个函数占用了
liuxingfffff 2014-01-23
  • 打赏
  • 举报
回复
闭包 是一段js代码。闭包 可以使用自身函数外的变量,且这个变量不是全局变量。 闭包的作用,防止局部变量 被js垃圾回收机机制回收。
十一文 2014-01-22
  • 打赏
  • 举报
回复
好文 不错学习了!
hookee 2014-01-20
  • 打赏
  • 举报
回复
xyxingyoucq 2014-01-20
  • 打赏
  • 举报
回复
学习了,呵呵,感觉不错
KazJ 2014-01-20
  • 打赏
  • 举报
回复
学习喽
张含韵 2014-01-20
  • 打赏
  • 举报
回复
引用 14 楼 u011986449 的回复:
这个可以整博客的、、、
同意
wangsufu77 2014-01-19
  • 打赏
  • 举报
回复
琉璃月-光 2014-01-19
  • 打赏
  • 举报
回复
嗯嗯,长知识了。。
加载更多回复(35)
JavaScript核心概念及实践》不仅帮助读者迅速掌握JavaScript基础知识和核心技术,而且通过实例讲解如何将这些知识和技术理解应用到实际工作中,提升编程能力,以简洁、优美的代码开发出功能强大且更易于维护和扩展的应用程序。 全书共16章和两个附录,可以分为两个部分。第一部分包括第1章到第7章,侧重介绍JavaScript语言的核心概念,为读者学习后续内容打下牢固基础;这部分介绍了Jav aScript的对象、函数、数组、正则表达式、闭包等主题,以及相关的重要知识点。第二部分包括第8章到第16章,着重讨论了JavaScript支持的编程范式、核心概念的延伸、JavaScript的前端开发框架、测试框架、JavaScript引擎、JavaScript在Java,C、C++等应用中的使用,以及服务器端的JavaScript应用。这部分内容更注重实例,帮助读者将核心概念引用到实际工作中。附录A介绍了一些常用JavaScript技巧;附录B简单介绍了jQuery并给出了一个应用实例。 《JavaScript核心概念及实践》可以供JavaScript初学者阅读,以快速学习和掌握这门语言的核心内容:对于有一定经验的JavaScript程序员,则可以通过本书加深和拓展对JavaScript的认识,提升应用开发能力。 目录: 第1章 概述 第2章 基本概念 第3章 对象 第4章 函数 第5章 数组 第6章 正则表达式 第7章 闭包 第8章 面向对象的JavaScript 第9章 函数式的JavaScript 第10章 核心概念深入 第11章 客户端的JavaScript 第12章 客户端的MVC框架:Backbone.js 第13章 JavaScript测试 第14章 JavaScript引擎 第15章 Java应用中的JavaScript 第16章 服务器端的JavaScript 附录A 一些JavaScript技巧 附录B 前端JavaScript框架:jQuery 第1章 概述   1.1 JavaScript简史   1.2 JavaScript语言特性   1.3 JavaScript应用范围  第2章 基本概念   2.1 数据类型   2.2 变量   2.3 运算符  第3章 对象   3.1 JavaScript对象   3.2 使用对象   3.3 对象字面量   3.4 JSON  第4章 函数   4.1 函数对象   4.2 函数作用域   4.3 函数上下文   4.4 call和apply   4.5 使用函数  第5章 数组   5.1 数组的特性   5.2 使用数组  第6章 正则表达式   6.1 正则表达式基础概念   6.2 使用正则表达式   6.3 实例:JSFilter  第7章 闭包  第8章 面向对象的JavaScript  第9章 函数式的JavaScript  第10章 核心概念深入  第11章 客户端的JavaScript  第12章 客户端的MVC框架:Backbone.js  第13章 JavaScript测试  第14章 JavaScript引擎  第15章 Java应用中的JavaScript  第16章 服务器端的JavaScript  附录A 一些JavaScript技巧  附录B 前端JavaScript框架:jQuery 

87,904

社区成员

发帖
与我相关
我的任务
社区描述
Web 开发 JavaScript
社区管理员
  • JavaScript
  • 无·法
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧