首页 > Web开发, 编程语言 > 理解Javascript的闭包

理解Javascript的闭包

2012年3月7日 发表评论 阅读评论 28,664 人阅读    

感谢 Neo 投递本文 – 微博帐号:_锟_

前言:还是一篇入门文章。Javascript中有几个非常重要的语言特性——对象、原型继承、闭包。其中闭包对于那些使用传统静态语言C/C++的程序员来说是一个新的语言特性。本文将以例子入手来介绍Javascript闭包的语言特性,并结合一点ECMAScript语言规范来使读者可以更深入的理解闭包。

注:本文是入门文章,例子素材整理于网络,如果你是高手,欢迎针对文章提出技术性建议和意见。本文讨论的是Javascript,不想做语言对比,如果您对Javascript天生不适,请自行绕道。

什么是闭包

闭包是什么?闭包是Closure,这是静态语言所不具有的一个新特性。但是闭包也不是什么复杂到不可理解的东西,简而言之,闭包就是:

  • 闭包就是函数的局部变量集合,只是这些局部变量在函数返回后会继续存在。
  • 闭包就是就是函数的“堆栈”在函数返回后并不释放,我们也可以理解为这些函数堆栈并不在栈上分配而是在堆上分配
  • 当在一个函数内定义另外一个函数就会产生闭包

上面的第二定义是第一个补充说明,抽取第一个定义的主谓宾——闭包是函数的‘局部变量’集合。只是这个局部变量是可以在函数返回后被访问。(这个不是官方定义,但是这个定义应该更有利于你理解闭包)

做为局部变量都可以被函数内的代码访问,这个和静态语言是没有差别。闭包的差别在于局部变变量可以在函数执行结束后仍然被函数外的代码访问。这意味着函数必须返回一个指向闭包的“引用”,或将这个”引用”赋值给某个外部变量,才能保证闭包中局部变量被外部代码访问。当然包含这个引用的实体应该是一个对象,因为在Javascript中除了基本类型剩下的就都是对象了。可惜的是,ECMAScript并没有提供相关的成员和方法来访问闭包中的局部变量。但是在ECMAScript中,函数对象中定义的内部函数(inner function)是可以直接访问外部函数的局部变量,通过这种机制,我们就可以以如下的方式完成对闭包的访问了。

function greeting(name) {
    var text = 'Hello ' + name; // local variable
    // 每次调用时,产生闭包,并返回内部函数对象给调用者
    return function() { alert(text); }
}
var sayHello=greeting("Closure");
sayHello()  // 通过闭包访问到了局部变量text

上述代码的执行结果是: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:闭包中局部变量是引用而非拷贝

function say667() {
    // Local variable that ends up within closure
    var num = 666;
    var sayAlert = function() { alert(num); }
    num++;
    return sayAlert;
}

var sayAlert = say667();
sayAlert()

因此执行结果应该弹出的667而非666。

例子2:多个函数绑定同一个闭包,因为他们定义在同一个函数内。

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

例子3:当在一个循环中赋值函数时,这些函数将绑定同样的闭包

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]();
    }
}

testList的执行结果是弹出item3 undefined窗口三次,因为这三个函数绑定了同一个闭包,而且item的值为最后计算的结果,但是当i跳出循环时i值为4,所以list[4]的结果为undefined.

例子4:外部函数所有局部变量都在闭包内,即使这个变量声明在内部函数定义之后。

function sayAlice() {
    var sayAlert = function() { alert(alice); }
    // Local variable that ends up within closure
    var alice = 'Hello Alice';
    return sayAlert;
}
var helloAlice=sayAlice();
helloAlice();

执行结果是弹出”Hello Alice”的窗口。即使局部变量声明在函数sayAlert之后,局部变量仍然可以被访问到。

例子5:每次函数调用的时候创建一个新的闭包

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'

闭包的应用

Singleton 单件:

var singleton = function () {
    var privateVariable;
    function privateFunction(x) {
        ...privateVariable...
    }

    return {
        firstMethod: function (a, b) {
            ...privateVariable...
        },
        secondMethod: function (c) {
            ...privateFunction()...
        }
    };
}();

这个单件通过闭包来实现。通过闭包完成了私有的成员和方法的封装。匿名主函数返回一个对象。对象包含了两个方法,方法1可以方法私有变量,方法2访问内部私有函数。需要注意的地方是匿名主函数结束的地方的’()’,如果没有这个’()’就不能产生单件。因为匿名函数只能返回了唯一的对象,而且不能被其他地方调用。这个就是利用闭包产生单件的方法。

参考:

JavaScript Closures For Dummies(镜像) 可惜都被墙了。
Advance Javascript (Douglas Crockford 大神的视频,一定要看啊)

(转载本站文章请注明作者和出处 酷 壳 – CoolShell.cn ,请勿用于任何商业用途)

——=== 访问 酷壳404页面 寻找遗失儿童。 ===——
好烂啊有点差凑合看看还不错很精彩 (13 人打了分,平均分: 5.00 )
Loading ... Loading ...
  1. pezy
    2012年3月7日08:55 | #1

    这几个例子的确不错。

  2. 2012年3月7日09:12 | #2

    学习了。。。

  3. sein_tao
    2012年3月7日09:16 | #3

    JS的闭包很特别, 跟perl/lisp里面的都不一样。可以讲讲动态语言中的closure与静态语言中的static变量/函数的区别不?

  4. 2012年3月7日09:18 | #4

    闭包解释通俗易懂,mark

  5. miniflycn
    2012年3月7日09:37 | #5

    = =这例子2能运行?!

  6. mokong
    2012年3月7日09:42 | #6

    楼上说的是这句 “setupSomeGolbals();” // 为三个全局变量赋值 这个单词 global 拼错了而已

    • Neo
      2012年3月7日22:14 | #7

      已经修改了,多谢

  7. jslhcl
    2012年3月7日10:09 | #8

    The Lexical Environment 还是翻译成“词法环境”好一点

  8. lltg
    2012年3月7日10:14 | #9

    个人感觉,其实单例我用到的并不多,反而闭包一个很有用的地方是回调函数,尤其是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]);
    然后就可以了……

  9. INNOCENT
    2012年3月7日10:19 | #11

    1) 静态语言也可以有闭包。google Go有,就连GNU C也有。
    2) 闭包的栈帧不一定要分配在堆上。放在栈上然后到处拷贝也是可以的。
    3) 在我看来,闭包就是函数返回之后没销毁的栈帧——其死期被拉长到闭包销毁时。
    以上均是对“什么是闭包”的一点看法,我不懂javascript :)

  10. 2012年3月7日10:22 | #12

    一些看不懂的英文。

  11. 吕子熏
    2012年3月7日13:35 | #13

    不用传入user_data,直接访问局部变量, 闭包在处理回调上确实是神器。 :)

  12. 2012年3月7日13:55 | #14

    我觉得第三个例子的解释可能有点问题,数组里的闭包应该是不同的,但是闭包里访问的item变量却是一样的,都是最后一次循环里的那个item

  13. Jan
    2012年3月7日14:53 | #15

    关于例子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’;

  14. 慵懒de土豆
    2012年3月7日15:08 | #16

    问个很菜鸟的问题,第2个例子,为啥那3个变量,不需要 var 定义?

  15. 慵懒de土豆
    2012年3月7日15:13 | #17

    哦,懂了。要定义成全局变量!!!

  16. 2012年3月8日11:36 | #18

    第3个例子:“当i跳出循环时i值为4”,跳出循环时i不应该是3吗?

  17. Liam
    2012年3月8日16:24 | #19

    @Kder
    因为 i++ 了

  18. 2012年3月9日12:38 | #20

    我感觉你闭包的最后一个定义有点问题,应该是函数内部定义的函数被外部变量引用了,才会引起闭包

  19. haha
    2012年3月12日10:42 | #21

    第3个例子有点问题,i的值应该是3

  20. 2012年3月12日14:40 | #22

    @慵懒de土豆
    JS中 var 是可以省略的。

  21. 2012年3月12日15:01 | #23

    闭包和 JSON 是 javascript 的两大法宝。
    Singleton的例子很多时候都要用到。

  22. david2003mn
    2012年3月12日17:13 | #24

    如果闭包用多了会不会造成内存泄漏

  23. RonQi
    2012年3月13日17:58 | #25

    文中“例子2:多个函数绑定同一个闭包,因为他们定义在同一个函数内。”
    请问为什么是绑定了同一个闭包呢?三个全局函数对应的function都不一样吧,不知道这里的“闭包”具体指的是什么?是“gAlertNumber ”等三个全局变量,还是对应三个匿名函数?

    • Neo
      2012年3月13日22:21 | #26

      因为这三个函数都在同一个外部函数中定义,所以这三个函数的指向的执行上下文环境是一样的。所以三个函数共享使用外部函数定义的局部变量num

  24. null
    2012年3月14日10:36 | #27

    弱问一句:闭包有啥用?

  25. 2012年3月15日16:50 | #28

    可以把外层函数理解成一个类,变量理解为静态成员,内部函数是成员方法。使用时是初始化一个对象~

    这样就更便于理解了。

    大师兄,我说的对吗?

  26. 2012年3月16日10:44 | #29

    真是不错啊! 支持一下

  27. lilyH
    2012年3月16日17:58 | #30

    又对js有了了解,感谢

  28. zhaoyou
    2012年3月21日09:36 | #31

    闭包的样列 =》第2个例子,最好不要列出来,这样会误导很多青年!

  29. 2012年3月23日22:36 | #32

    谢谢,终于对闭包有了点感性的认识!

  30. 2012年3月24日01:50 | #33

    @Liam
    list.length=3,循环条件是 i < list.length,当i=3时就跳出了,后面的i++就不应该执行了啊,我测试了,for循环结束后i值就是3啊

    PS:我是新手,希望前辈悉心指教~

  31. 2012年3月25日16:57 | #34

    学习了,很好的文章,这是我第一次看JS闭包的文章,说的很清楚。

  32. flyking
    2012年3月28日17:58 | #35

    谢谢了,写的真好!

  33. 2012年4月24日00:38 | #36

    http://xmuzyq.iteye.com/blog/1198717 我之前也写过一篇文章介绍这个的,各位可以交流下。

  34. JazzCoder
    2012年8月24日18:33 | #37

    大神啊~!

  35. skywalker
    2012年10月23日12:08 | #38

    作者对闭包的解释有问题,对闭包的创建和销毁机制也没涉及,建议参考Nicholas Zakas写的<>一书中对闭包的剖析。

  36. darkhorse
    2012年10月26日16:59 | #39

    例子2中函数内部未使用var声明的变量,这个太容易误导入门者了.

  37. qewewqe
    2013年1月5日18:13 | #40

    Sanitizing !

  38. qqbunny
    2013年2月4日19:28 | #41

    var singleton = {}; 是单例的吗?

  39. 2013年5月8日13:23 | #42

    写的真好!赞一个

  40. new
    2013年5月29日10:28 | #43

    @lltg
    结果会发现把所有人都状态都加到了最后一个人身上……

    我写了个测试,不用回调不会发生你说的这种情况。我的测试代码

    l = document.getElementById(“xx”).rows;

    for (i = 0; i < l.length; i++) {
    l[i].cells[0].innerHTML += getState(l[i].cells[0].getAttribute('uid'));
    }

    function getState(uid) {
    return "status: " + uid + uid;
    }

    显示结果:
    status: 11
    status: 22
    status: 33
    status: 44
    status: 55

    不知道是不是我理解的不对,请指正

  41. kabaskimy
    2013年11月4日10:45 | #44

    写的不错,但是有几个槽点..
    1、第三个例子中的i应该是3,是从0,1,2,3这样变化来的
    2、第四个例子其实用javascript的变量声明提前就可以解释了

  42. 2013年12月9日22:14 | #45

    例子3:当在一个循环中赋值函数时,这些函数将绑定同样的闭包。这个解析应该是错的。
    function testSame() {
    var fnlist = buildList([1,2,3]);
    alert(fnlist[0] == fnlist[1]); // 这里会打印 false, 而不是true
    }

    正确的解释应该是, for循环中的 var item 并没有被重新定义,只是被重新赋值了而已,故循环里面的3个闭包中item的值是一致的。在闭包中打印 Math.random() 即可证明。

    function buildList(list) {
    var result = [];
    for (var i = 0; i < list.length; i++) {
    result.push( function() {alert(Math.random())} );
    }
    return result;
    }

  43. Tony
    2013年12月24日01:59 | #46

    理解了闭包模型就很容易理解这几个例子了 单件例子是否可以这样理解
    var singleton =( function(){ ……;} ) ( ); ?

  44. yuefeng
    2014年2月20日00:16 | #47

    @Kder
    是的,就是3,但list[3]=undefined。

  45. yuefeng
    2014年2月20日00:20 | #48

    感谢分享!

  46. caoshuai
    2014年3月13日10:36 | #49

    老师您好,问您一个菜鸟的问题, 对于您第一个例子sayHello(), 并无法弹出Hello Closure,

  47. xwqfudimo
    2014年3月28日17:06 | #50

    @dodo
    照你这个比喻,go语言闭包内的变量不是静态变量,因为不同的闭包实例互不影响。不知道javascript闭包是不是这样…

  1. 2012年3月8日10:52 | #1
  2. 2012年3月8日18:49 | #2
  3. 2012年3月11日01:10 | #3
  4. 2012年3月13日00:50 | #4
  5. 2012年4月15日12:11 | #5
  6. 2012年5月25日02:00 | #6
  7. 2012年5月25日14:18 | #7
  8. 2012年5月25日16:25 | #8
  9. 2012年8月9日00:54 | #9
  10. 2012年9月28日01:20 | #10
  11. 2013年1月13日17:52 | #11
  12. 2013年5月11日23:42 | #12
  13. 2013年5月25日15:54 | #13
  14. 2013年6月18日10:52 | #14
  15. 2013年7月24日17:21 | #15
  16. 2013年8月31日17:37 | #16
  17. 2013年12月5日00:23 | #17
  18. 2013年12月30日22:10 | #18
  19. 2014年1月7日02:22 | #19