闭包(closure)是什么东西

我面试前端基本都会问一个问题"请描述一下闭包"。相当多的应聘者的反应都是断断续续的词,“子函数”“父函数”“变量”,支支吾吾的说不清楚。我提示说如果你表述不清楚你可以写一小段代码示例一下。这个基本都会,比如这样:

  1. function A() {
  2. var i = 0;
  3. return function(){return i++;}
  4. }

看得出来他知道什么叫闭包,但是却又不清楚,只知道这么个写法,但是却又不是足够熟悉。那么本文就来深入探讨一下,闭包到底是个什么东西。

从C语言开始

 
我又要从C语言开始了。没办法,讲细节就得从C语言开始。刚开始学C语言的时候,教材上面都讲过,变量作用域分两种,全局变量,局部变量。全局变量很好理解,就是程序的全局都可以访问的变量。那局部变量呢?就是在一个子程序内部才可以访问的变量。这句话换一个意思就是,子程序外部将访问不了这个变量。有人说不对,我可以把局部变量返回啊。的确,把局部变量返回就可以在函数的外部来得到局部变量的值了。但事实上,当子函数返回的时候,返回值并不是局部变量本身,而是局部变量的一个拷贝值。我们以一个最简单的例子来描述这个局部变量返回的过程,比如这个子函数:
  1. int calc(int a, int b) {
  2. int tempA = a * a;
  3. int tempB = b * b;
  4. int c = tempA + tempB;
  5. return c;
  6. }
  7. int main() {
  8. int sum = calc(3, 4);
  9. return 0;
  10. }

当函数calc调用的时候,main函数将两个参数,3,4压栈,然后进入calc函数。calc函数中用两个局部变量tempA,tempB来保存它们的平方。计算完和之后,将平方和返回给main函数。那么calc返回之后,如何访问tempA,tempB呢?答案是没法访问。calc函数退栈之后,这俩局部变量销毁了。局部变量的生命周期就是在他所在的子函数执行的时候存在,执行完成之后就销毁,赤裸裸的兔死狗烹。所以局部变量经常被叫做临时变量,这确实名副其实。

好,讲那么一大堆,那么和今天的主角闭包有什么关系呢?我讲这个例子,目的就是强调一点,局部变量都在栈中,在函数返回之后退栈,退栈之后局部变量通通销毁。
 
下面再讲一个概念就是上下文。对于一个函数的上下文就是这个函数能够访问的变量。比如calc函数,它的上下文就是全局变量(例子里面没有,但明显它是可以访问的),还有局部变量,就是传入参数加上函数内部声明的变量。这个函数就只能接触这些东西,这些东西的集合就是上下文。所以很明显一个函数是离不开上下文的。

再来讲闭包

 
回到本文一开始的那个JS函数,当这个函数被调用,比如var func = A(). 那么此时func就是A返回的一个函数。如果套用刚才讲的,A返回之后退栈,局部变量销毁,那么i变量就没有了。但事实上,func这个函数是可以调用的,反复调用就返回一个递增序列。递增所用的那个计数器变量,就是i,它没有被销毁,看起来这个函数的调用过程好像和C里面的不一样啊。C函数中,一个子程序的上下文就是栈中的局部变量(解释的时候方便起见,全局变量就不讲了,因为这个容易理解,并且C和JS在全局变量方面很相似),没有其他的东西了,退栈之后这些东西也就销毁了。但是JS函数的上下文除了局部变量之外,还有一个东西就是闭包。下面的图描述了一个有闭包的JS函数的上下文。
被调用的函数除了能够访问自己的局部变量var1,var2,它还有一个指针,顺着这个指针指向的内容就是他的父函数的局部变量空间。尽管此时它的父函数已经退栈了,由于子函数还存在,这个存在可能是被返回,或者是被赋值给了一个全局变量,父函数的局部变量就会被复制到堆中,在堆中保存了一个仅供子函数访问的一个上下文。所以这个时候子函数的上下文就是局部变量+父函数的局部变量。
所以说白了,闭包就一个供子函数访问的上下文,一组父函数的局部变量而已。

闭包的特性

 
首先,闭包中的变量只有子函数才能访问,其他的没有任何手段可以访问。所以使用这个特性可以制造一个安全的空间来保存一些变量。比如文章最初的JS例子,将计数器放到了闭包之中,这个i绝对没有其他的手段访问,用这种手段可以确保i不会被恶意修改,使得它能够安全的返回一个递增序列。此外这个特性还可以在大型项目中保证全局名空间不被污染,让多个模块可以安全的协作。比如两个项目组分别开发两个JS的库函数,这个库函数中包括了很多的函数和变量。那么代码就会变成这样:>

  1. //modue1
  2. var m1,m2,m3;
  3. function foo1(){...}
  4. function foo2(){...}
  5. //module2
  6. var s1,s2,s3;
  7. function sb1(){...}
  8. function sb2(){...}

代码中引入了这个两个JS文件module1和module2.如果变量,函数较多,万一出现重名了,别的模块的变量就会被莫名其妙的修改,那么一些匪夷所思的bug就会出来。为了避免重名,当然可以规定前缀,命名规范等等,但是这治标不治本。让开发人员麻烦不说,当模块变多,维护这种规范的难度就会明显上升。结合刚才的闭包的特性,所以我们可以吧代码改成这样:

  1. //modue1
  2. function module1(){
  3. var m1,m2,m3;
  4. function foo1(){...}
  5. function foo2(){...}
  6. return{
  7. "foo1":foo1,
  8. "foo2":foo2
  9. }
  10. }
  11. //module2
  12. function module2(){
  13. var s1,s2,s3;
  14. function sb1(){...}
  15. function sb2(){...}
  16. return{
  17. "sb1":sb1,
  18. "sb2":sb2
  19. }
  20. }

当使用一个模块的时候,调用这个模块对应的函数即可,顶层的名空间就是模块的名字,这样维护起来就会方便很多。当然,大型项目中的多模块协作用闭包是一个手段,还有一个手段就是面向对象,这个我以后的文章会讲到。

闭包的另外一个容易忽略的特性就是一个闭包内的变量会被这个闭包对应的所有子函数共享。说起来有点拗口,我们来举个例子:

  1. function test() {
  2. var array = [];
  3. for(var i = 0; i < 10; i++) {
  4. array[i] = function(){return i;}
  5. }
  6. return array;
  7. }

这段代码返回了一个数组,数组里面的每一个元素都是一个函数。那么有下面的测试代码:

  1. var a = test();
  2. a[0](); //返回几?

看起来它应该返回0啊。这个函数产生的时候i = 0。这就错了,事实上这个函数会返回10,并且这个数组里面所有的函数都返回10.为什么呢?刚才有提到,数组中的所有的函数共享一个闭包,就是test函数的局部变量i。当test执行完成之后,i是几呢?当然是10.所以数组中的函数就都返回10了。这个特性经常被刚接触闭包的程序员忽视。当制造闭包的函数中有循环,条件语句的时候,经常会出现错误。这个要小心了。

此外,闭包是可以嵌套的。相信如果你前面的内容看懂了的话,理解嵌套闭包就是易如反掌了,在此我不做赘述。

结束

扯了那么多,以一言蔽之,闭包是什么?闭包就是父函数返回时候,复制到堆中的供子函数使用的上下文。

JavaScript 进阶(四)解密闭包closure的更多相关文章

  1. JavaScript进阶(四)js字符串转换成数字的三种方法

    js字符串转换成数字的三种方法 在js读取文本框或者其它表单数据的时候获得的值是字符串类型的,例如两个文本框a和b,如果获得a的value值为11,b的value值为9 ,那么a.value要小于b. ...

  2. 深入理解JavaScript闭包(closure)

    最近在网上查阅了不少javascript闭包(closure)相关的资料,写的大多是非常的学术和专业.对于初学者来说别说理解闭包了,就连文字叙述都很难看懂.撰写此文的目的就是用最通俗的文字揭开Java ...

  3. javascript中的闭包(Closure)的学习

    闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现. 下面是我在网上通过学习阮一峰老师的笔记,感觉总结很不错,特记录于此. 一.变量的作用域 要理解 ...

  4. [转载]学习Javascript闭包(Closure)

    学习Javascript闭包(Closure)     源地址: http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures ...

  5. javascript进阶课程--第三章--匿名函数和闭包

    javascript进阶课程--第三章--匿名函数和闭包 一.总结 二.学习要点 掌握匿名函数和闭包的应用 三.匿名函数和闭包 匿名函数 没有函数名字的函数 单独的匿名函数是无法运行和调用的 可以把匿 ...

  6. 前端进阶必读:《JavaScript核心技术开发解密》核心提炼二

    前言 最近读勒基本关于前端的数据<JavaScript核心技术开发解密>,<webpack从入门到进阶>...这几本书帮助到我更好的理解JS.webpack在前端技术领域中的作 ...

  7. JavaScript闭包(Closure)

    JavaScript闭包(Closure) 本文收集了多本书里对JavaScript闭包(Closure)的解释,或许会对理解闭包有一定帮助. <你不知道的JavsScript> Java ...

  8. 【转】深入理解JavaScript闭包闭包(closure) (closure)

    一.什么是闭包?"官方"的解释是:闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分.相信很少有人能直接看懂这句话,因为他描述 ...

  9. JavaScript进阶系列01,函数的声明,函数参数,函数闭包

    本篇主要体验JavaScript函数的声明.函数参数以及函数闭包. □ 函数的声明 ※ 声明全局函数 通常这样声明函数: function doSth() { alert("可以在任何时候调 ...

随机推荐

  1. 为joomla加入�下拉菜单的方法

    用 Joomla! 建站的大多数站长都须要在站点前台使用下拉菜单(dropdown menu),或者叫弹出菜单(slide menu),由于这样能够在有限的页面空间上公布很多其它的导航菜单,而且能够进 ...

  2. Linux下which、whereis、locate、find 区别

    我们经常在linux要查找某个文件或命令,但不知道放在哪里了,可以使用下面的一些命令来搜索.which      查看可执行文件的位置 whereis    查看文件的位置 locate     配合 ...

  3. centos 安装lua

    yum install readline-develwget http://www.lua.org/ftp/lua-5.1.4.tar.gztar -xzvf lua-5.1.4.tar.gz3.编译 ...

  4. [javascript]一种兼容性比较好的简单拖拽

    作为一个马上要找工作.非计算机专业.热爱前端的大四狗,最近开始疯狂写demo.看书,准备九.十月份的校招. 晚上用js实现了一个比较简单(low)的拖拽效果,初步测试兼容性还是不错的,于是写一段小博文 ...

  5. POJ 1379 Run Away 【基础模拟退火】

    题意:找出一点,距离所有所有点的最短距离最大 二维平面内模拟退火即可,同样这题用最小圆覆盖也是可以的. Source Code: //#pragma comment(linker, "/ST ...

  6. 界面控件 - 滚动条ScrollBar(对滚动条消息和鼠标消息结合讲的不错)

    界面是人机交互的门户,对产品至关重要.在界面开发中只有想不到没有做不到的,有好的想法,当然要尝试着做出来.对滚动条的扩展,现在有很多类是的例子. VS2015的代码编辑是非常强大的,其中有一个功能可以 ...

  7. c# 数据库编程(通过SqlCommand 执行数据库查询)

    前面一篇文章,我们介绍了如何在c#中对数据库进行更新操作.主要是利用SqlCommand 对象的ExecuteNonQuery方法. 这篇文章介绍,如何进行查询操作.本文给出的例子仍然是针对sql s ...

  8. hdu1853解题报告

    题意和解决回路匹配的思路如同hdu3488 (这里我第一次想到最短路,但是对于有回路这个不知道怎么处理,后来看了别人的解题报告才知道KM匹配,但是看到KM之后就自己想...想了很久....还是不知道回 ...

  9. BZOJ 2510: 弱题( 矩阵快速幂 )

    每进行一次, 编号为x的数对x, 和(x+1)%N都有贡献 用矩阵快速幂, O(N3logK). 注意到是循环矩阵, 可以把矩阵乘法的复杂度降到O(N2). 所以总复杂度就是O(N2logK) --- ...

  10. Android_简单笔记一

    入门学习Android的简单笔记(已经安装好了开发环境ADT) 一.关于 AndroidManifest.xml文件 1. android:icon和android:label定义了应用程序安装后显示 ...