理解Javascript的闭包
【感谢 Neo 投递本文 – 微博帐号:_锟_ 】
前言:还是一篇入门文章。Javascript中有几个非常重要的语言特性——对象、原型继承、闭包。其中闭包对于那些使用传统静态语言C/C++的程序员来说是一个新的语言特性。本文将以例子入手来介绍Javascript闭包的语言特性,并结合一点ECMAScript语言规范来使读者可以更深入的理解闭包。
注:本文是入门文章,例子素材整理于网络,如果你是高手,欢迎针对文章提出技术性建议和意见。本文讨论的是Javascript,不想做语言对比,如果您对Javascript天生不适,请自行绕道。
目录
什么是闭包
闭包是什么?闭包是Closure,这是静态语言所不具有的一个新特性。但是闭包也不是什么复杂到不可理解的东西,简而言之,闭包就是:
- 闭包就是函数的局部变量集合,只是这些局部变量在函数返回后会继续存在。
- 闭包就是就是函数的“堆栈”在函数返回后并不释放,我们也可以理解为这些函数堆栈并不在栈上分配而是在堆上分配
- 当在一个函数内定义另外一个函数就会产生闭包
上面的第二定义是第一个补充说明,抽取第一个定义的主谓宾——闭包是函数的‘局部变量’集合。只是这个局部变量是可以在函数返回后被访问。(这个不是官方定义,但是这个定义应该更有利于你理解闭包)
做为局部变量都可以被函数内的代码访问,这个和静态语言是没有差别。闭包的差别在于局部变变量可以在函数执行结束后仍然被函数外的代码访问。这意味着函数必须返回一个指向闭包的“引用”,或将这个”引用”赋值给某个外部变量,才能保证闭包中局部变量被外部代码访问。当然包含这个引用的实体应该是一个对象,因为在Javascript中除了基本类型剩下的就都是对象了。可惜的是,ECMAScript并没有提供相关的成员和方法来访问闭包中的局部变量。但是在ECMAScript中,函数对象中定义的内部函数(inner function)是可以直接访问外部函数的局部变量,通过这种机制,我们就可以以如下的方式完成对闭包的访问了。
[javascript]
function greeting(name) {
var text = ‘Hello ‘ + name; // local variable
// 每次调用时,产生闭包,并返回内部函数对象给调用者
return function() { alert(text); }
}
var sayHello=greeting("Closure");
sayHello() // 通过闭包访问到了局部变量text
[/javascript]
上述代码的执行结果是:Hello Closure,因为sayHello()函数在greeting函数执行完毕后,仍然可以访问到了定义在其之内的局部变量text。
好了,这个就是传说中闭包的效果,闭包在Javascript中有多种应用场景和模式,比如Singleton,Power Constructor等这些Javascript模式都离不开对闭包的使用。
ECMAScript闭包模型
ECMAScript到底是如何实现闭包的呢?想深入了解的亲们可以获取ECMAScript 规范进行研究,我这里也只做一个简单的讲解,内容也是来自于网络。
在ECMAscript的脚本的函数运行时,每个函数关联都有一个执行上下文场景(Execution Context) ,这个执行上下文场景中包含三个部分
- 文法环境(The LexicalEnvironment)
- 变量环境(The VariableEnvironment)
- this绑定
其中第三点this绑定与闭包无关,不在本文中讨论。文法环境中用于解析函数执行过程使用到的变量标识符。我们可以将文法环境想象成一个对象,该对象包含了两个重要组件,环境记录(Enviroment Recode),和外部引用(指针)。环境记录包含包含了函数内部声明的局部变量和参数变量,外部引用指向了外部函数对象的上下文执行场景。全局的上下文场景中此引用值为NULL。这样的数据结构就构成了一个单向的链表,每个引用都指向外层的上下文场景。
例如上面我们例子的闭包模型应该是这样,sayHello函数在最下层,上层是函数greeting,最外层是全局场景。如下图:
因此当sayHello被调用的时候,sayHello会通过上下文场景找到局部变量text的值,因此在屏幕的对话框中显示出”Hello Closure”
变量环境(The VariableEnvironment)和文法环境的作用基本相似,具体的区别请参看ECMAScript的规范文档。
闭包的样列
前面的我大致了解了Javascript闭包是什么,闭包在Javascript是怎么实现的。下面我们通过针对一些例子来帮助大家更加深入的理解闭包,下面共有5个样例,例子来自于JavaScript Closures For Dummies(镜像)。
例子1:闭包中局部变量是引用而非拷贝
[javascript]
function say667() {
// Local variable that ends up within closure
var num = 666;
var sayAlert = function() { alert(num); }
num++;
return sayAlert;
}
var sayAlert = say667();
sayAlert()
[/javascript]
因此执行结果应该弹出的667而非666。
例子2:多个函数绑定同一个闭包,因为他们定义在同一个函数内。
[javascript]
function setupSomeGlobals() {
// Local variable that ends up within closure
var num = 666;
// Store some references to functions as global variables
gAlertNumber = function() { alert(num); }
gIncreaseNumber = function() { num++; }
gSetNumber = function(x) { num = x; }
}
setupSomeGlobals(); // 为三个全局变量赋值
gAlertNumber(); //666
gIncreaseNumber();
gAlertNumber(); // 667
gSetNumber(12);//
gAlertNumber();//12
[/javascript]
例子3:当在一个循环中赋值函数时,这些函数将绑定同样的闭包
[javascript]
function buildList(list) {
var result = [];
for (var i = 0; i < list.length; i++) {
var item = ‘item’ + list[i];
result.push( function() {alert(item + ‘ ‘ + list[i])} );
}
return result;
}
function testList() {
var fnlist = buildList([1,2,3]);
// using j only to help prevent confusion – could use i
for (var j = 0; j < fnlist.length; j++) {
fnlist[j]();
}
}
[/javascript]
testList的执行结果是弹出item3 undefined窗口三次,因为这三个函数绑定了同一个闭包,而且item的值为最后计算的结果,但是当i跳出循环时i值为4,所以list[4]的结果为undefined.
例子4:外部函数所有局部变量都在闭包内,即使这个变量声明在内部函数定义之后。
[javascript]
function sayAlice() {
var sayAlert = function() { alert(alice); }
// Local variable that ends up within closure
var alice = ‘Hello Alice’;
return sayAlert;
}
var helloAlice=sayAlice();
helloAlice();
[/javascript]
执行结果是弹出”Hello Alice”的窗口。即使局部变量声明在函数sayAlert之后,局部变量仍然可以被访问到。
例子5:每次函数调用的时候创建一个新的闭包
[javascript]
function newClosure(someNum, someRef) {
// Local variables that end up within closure
var num = someNum;
var anArray = [1,2,3];
var ref = someRef;
return function(x) {
num += x;
anArray.push(num);
alert(‘num: ‘ + num +
‘\nanArray ‘ + anArray.toString() +
‘\nref.someVar ‘ + ref.someVar);
}
}
closure1=newClosure(40,{someVar:’closure 1′});
closure2=newClosure(1000,{someVar:’closure 2′});
closure1(5); // num:45 anArray[1,2,3,45] ref:’someVar closure1′
closure2(-10);// num:990 anArray[1,2,3,990] ref:’someVar closure2’
[/javascript]
闭包的应用
Singleton 单件:
[javascript]
var singleton = function () {
var privateVariable;
function privateFunction(x) {
…privateVariable…
}
return {
firstMethod: function (a, b) {
…privateVariable…
},
secondMethod: function (c) {
…privateFunction()…
}
};
}();
[/javascript]
这个单件通过闭包来实现。通过闭包完成了私有的成员和方法的封装。匿名主函数返回一个对象。对象包含了两个方法,方法1可以方法私有变量,方法2访问内部私有函数。需要注意的地方是匿名主函数结束的地方的'()’,如果没有这个'()’就不能产生单件。因为匿名函数只能返回了唯一的对象,而且不能被其他地方调用。这个就是利用闭包产生单件的方法。
参考:
JavaScript Closures For Dummies(镜像) 可惜都被墙了。
Advance Javascript (Douglas Crockford 大神的视频,一定要看啊)
(转载本站文章请注明作者和出处 酷 壳 – CoolShell ,请勿用于任何商业用途)
《理解Javascript的闭包》的相关评论
这几个例子的确不错。
学习了。。。
JS的闭包很特别, 跟perl/lisp里面的都不一样。可以讲讲动态语言中的closure与静态语言中的static变量/函数的区别不?
闭包解释通俗易懂,mark
= =这例子2能运行?!
楼上说的是这句 “setupSomeGolbals();” // 为三个全局变量赋值 这个单词 global 拼错了而已
已经修改了,多谢
The Lexical Environment 还是翻译成“词法环境”好一点
个人感觉,其实单例我用到的并不多,反而闭包一个很有用的地方是回调函数,尤其是Ajax回调之类。
比如我们有一个联系人列表,然后现在要用ajax拉取每个人的在线状态显示在人名后面,后端已经固定好了一次只能拉取一个(有时是因为你写的是油猴脚本,后端你根本奈何不得)。
一个for循环对每个人做操作比如
for (i = 0; i < l.length; i++)
l[i].innerHTML += getState(l[i].getAttribute('uid'));
结果会发现把所有人都状态都加到了最后一个人身上……
换闭包
for (i = 0; i < l.length; i++)
(function (p) { p.innerHTML += getState(p.getAttribute('uid')); })(l[i]);
然后就可以了……
不错。
1) 静态语言也可以有闭包。google Go有,就连GNU C也有。
2) 闭包的栈帧不一定要分配在堆上。放在栈上然后到处拷贝也是可以的。
3) 在我看来,闭包就是函数返回之后没销毁的栈帧——其死期被拉长到闭包销毁时。
以上均是对“什么是闭包”的一点看法,我不懂javascript :)
一些看不懂的英文。
不用传入user_data,直接访问局部变量, 闭包在处理回调上确实是神器。 :)
我觉得第三个例子的解释可能有点问题,数组里的闭包应该是不同的,但是闭包里访问的item变量却是一样的,都是最后一次循环里的那个item
关于例子4,要表达的并非闭包问题,主要是JS的解释代码过程存在的问题。
其实就是
function sayAlice() {
var sayAlert = function() { alert(alice); }
// Local variable that ends up within closure
return sayAlert;
}
var helloAlice=sayAlice();
var alice = ‘Hello Alice’;
helloAlice(); // Hello Alice
和
function sayAlice() {
var sayAlert = function() { alert(alice); }
// Local variable that ends up within closure
return sayAlert;
}
var helloAlice=sayAlice();
helloAlice(); // undefined
var alice = ‘Hello Alice’;
的差异。
JS在解释代码时,会把变量的定义提升到代码的“最顶”。
实际效果如下:
var alice;
function sayAlice() {
var sayAlert = function() { alert(alice); }
// Local variable that ends up within closure
return sayAlert;
}
var helloAlice=sayAlice();
helloAlice(); // undefined
alice = ‘Hello Alice’;
问个很菜鸟的问题,第2个例子,为啥那3个变量,不需要 var 定义?
哦,懂了。要定义成全局变量!!!
第3个例子:“当i跳出循环时i值为4”,跳出循环时i不应该是3吗?
@Kder
因为 i++ 了
我感觉你闭包的最后一个定义有点问题,应该是函数内部定义的函数被外部变量引用了,才会引起闭包
第3个例子有点问题,i的值应该是3
@慵懒de土豆
JS中 var 是可以省略的。
闭包和 JSON 是 javascript 的两大法宝。
Singleton的例子很多时候都要用到。
如果闭包用多了会不会造成内存泄漏
文中“例子2:多个函数绑定同一个闭包,因为他们定义在同一个函数内。”
请问为什么是绑定了同一个闭包呢?三个全局函数对应的function都不一样吧,不知道这里的“闭包”具体指的是什么?是“gAlertNumber ”等三个全局变量,还是对应三个匿名函数?
因为这三个函数都在同一个外部函数中定义,所以这三个函数的指向的执行上下文环境是一样的。所以三个函数共享使用外部函数定义的局部变量num
弱问一句:闭包有啥用?
可以把外层函数理解成一个类,变量理解为静态成员,内部函数是成员方法。使用时是初始化一个对象~
这样就更便于理解了。
大师兄,我说的对吗?
真是不错啊! 支持一下
又对js有了了解,感谢
闭包的样列 =》第2个例子,最好不要列出来,这样会误导很多青年!
谢谢,终于对闭包有了点感性的认识!
@Liam
list.length=3,循环条件是 i < list.length,当i=3时就跳出了,后面的i++就不应该执行了啊,我测试了,for循环结束后i值就是3啊
PS:我是新手,希望前辈悉心指教~
学习了,很好的文章,这是我第一次看JS闭包的文章,说的很清楚。
谢谢了,写的真好!
http://xmuzyq.iteye.com/blog/1198717 我之前也写过一篇文章介绍这个的,各位可以交流下。
大神啊~!
作者对闭包的解释有问题,对闭包的创建和销毁机制也没涉及,建议参考Nicholas Zakas写的<>一书中对闭包的剖析。
例子2中函数内部未使用var声明的变量,这个太容易误导入门者了.
Sanitizing !
var singleton = {}; 是单例的吗?
写的真好!赞一个