阅读目录


闭包在javascript来说是比较重要的概念,平时工作中也是用的比较多的一项技术。下来对其进行一个小小的总结


​回到顶部​

什么是闭包?


官方说法:

闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数,通过另一个函数访问这个函数的局部变量------《javascript高级程序设计第三版》


下面就是一个简单的闭包:


​function​​​​A(){​​​​​​​​var​​​​text=​​​​"hello world"​​​​;​​​​​​​​function​​​​B(){​​​​​​​​console.log(text);​​​​​​​​}​​​​​​​​return​​​​B;​​​​}​​​​var​​​​c=A();​​​​c(); ​​​​// hello world ​


按照字面量的意思是:函数B有权访问函数A作用域中的变量(text),通过另一个函数C来访问这个函数的局部变量text。因此函数B形成了一个闭包。也可以说C是一个闭包,因为C执行的实际是函数B。

这个需要注意的是,直接执行A();是没有任何反应的。因为return B没有执行,除非是return B();

​回到顶部​

闭包的特性


闭包有三个特性:

 1.函数嵌套函数
2.函数内部可以引用外部的参数和变量
3.参数和变量不会被垃圾回收机制回收

解释一下第3点,为什么闭包的参数和变量不会被垃圾回收机制回收呢?

首先我们先了解一下javascript的垃圾回收原理:

(1)、在javascript中,如果一个对象不再被引用,那么这个对象就会被​​GC(garbage collection)​​回收;

(2)、如果两个对象互相引用,而不再被第​​3​​者所引用,那么这两个互相引用的对象也会被回收。

上面的示例代码中A是B的父函数,而B被赋给了一个全局变量C(全局变量的生命周期直至浏览器卸载页面才会结束),这导致B始终在内存中,而B的存在依赖于A,因此A也始终在内存中,不会在调用结束后,被垃圾回收机制(garbage collection)回收。

​回到顶部​

闭包的作用:


其实闭包的作用也是有闭包的特性决定的,根据上面的闭包特性,闭包的作用如下:

1、可以读取函数内部的变量,而不是定义一起全局变量,避免污染环境

2、让这些变量的值始终保持在内存中。

​回到顶部​

闭包的代码示例


下面主要介绍几种常见的闭包,并进行解析:


demo1 局部变量的累加。


1 2 3 4 5 6 7 8 9 10 11 12

​function​​​​countFn(){​​​​​​​​var​​​​count=1;​​​​​​​​return​​​​function​​​​(){ ​​​​//函数嵌套函数​​​​​​​​count++;​​​​​​​​console.log(count);​​​​​​​​}​​​​}​​​​var​​​​y = countFn(); ​​​​//外部函数赋给变量y;​​​​y(); ​​​​//2 //y函数调用一次,结果为2,相当于countFn()()​​​​y(); ​​​​//3 //y函数调用第二次,结果为3,因为上一次调用的count还保存在内存中,没有被销毁,所以实现了累加​​​​y=​​​​null​​​​; ​​​​//垃圾回收,释放内存​​​​y(); ​​​​// y is not a function​


由于第一次执行完,变量count还保存在内存中,所以不会被回收,以致于第二次执行的时候可以对上次的值就行累加。当引入y=null时,销毁引用,释放内存


demo2 循环中使用闭包

代码如下(下面的三个代码示例):我们的目的是想在每次循环中调用循环序号:


demo2-1


for (var i = 0; i < 10; i++) {     var a = function(){         console.log(i)     }     a()  //依次为0--9 }


这个例子的结果是没有题的,我们依次打印出了0-9

每一层匿名函数和变量i都组成了一个闭包,但是这样在循环中并没有问题,因为函数在循环体中立即被执行了


demo2-2

但是在setTimeout中就不一样了


1 2 3 4 5

​for​​​​(​​​​var​​​​i = 0; i < 10; i++) {​​​​​​​​setTimeout(​​​​function​​​​() {​​​​​​​​console.log(i); ​​​​//10次10​​​​​​​​}, 1000);​​​​}​


我们期望的依次是打印出0--10,实际情况是打印出 10次10。即使吧setTimeout的时间改为0,也是打印出10个10。这是为什么呢?

这是因为​​setTimeout​​​的一种机制,​​setTimeout​​是从任务队列结束的时候开始计时的,如果前面有进程没有结束,那么它就等到它结束再开始计时。在这里,任务队列就是它自己所在的循环。

循环结束​​setTimeout​​​才开始计时,所以无论如何,​​setTimeout​​里面的i都是最后一次循环的 i。该代码中,最后的 i 为10,所以打印出了10个10.

这也就是为什么​​setTimeout​​的回调不是每次取循环时的值,而取最后一次的值


demo2-3

解决上面的setTimeout不能依次打印出循环的问题

【原】理解javascript中的闭包(***********************************************)_回调函数

for(var i=0;i<10;i++){     var a=function(e){         return function(){              console.log(e); //依次输入0--9         }     }     setTimeout(a(i),0); }

【原】理解javascript中的闭包(***********************************************)_回调函数

因为​​setTimeout​​第一个参数需要一个函数,所以返回一个函数给它,返回的同时把 i 作为参数传进去,通过形参 e 缓存了i,也就是说e变量相当于是 i 的一个拷贝 ,并带进返回的函数里面。

当 ​​setTimeout​​​ 的执行时,它就拥有了对 ​​e​​ 的引用,而这个值是不会被循环改变的。


也可以用下面的写法,和上面类似:

【原】理解javascript中的闭包(***********************************************)_回调函数

for(var i = 0; i < 10; i++) {     (function(e) {         setTimeout(function() {             console.log(e);  //依次打印出0-9         }, 0);     })(i); }

【原】理解javascript中的闭包(***********************************************)_回调函数


demo3 循环中添加事件

看下面的一个典型的demo.

我们希望每次点击li的时候,alert出li的索引值,所以用下面的代码:

【原】理解javascript中的闭包(***********************************************)_回调函数

<ul id="test">     <li>第一个</li>     <li>第二个</li>     <li>第三个</li>     <li>第四个</li> </ul>  var nodes = document.getElementsByTagName("li"); for(i = 0,len=nodes.length;i<len;i++){     nodes[i].onclick = function(){         alert(i);   //值全是4     }; }

【原】理解javascript中的闭包(***********************************************)_回调函数

事与愿违,无论点击哪一个li,都是alert(4),也就是都是alert循环结束之后的索引值。这是为什么呢?

这是因为循环中为不同的元素绑定事件,事件回调函数里如果调用了跟循环相关的变量,则这个变量取循环的最后一个值。

由于绑定的回调函数是一个匿名函数,所以上面的代码中, 这个匿名函数是一个闭包,携带的作用域为外层作用域(也就是for里面的作用域),当事件触发的时候,作用域中的变量已经随着循环走到最后了。

还有一点就是,事件是需要触发的,而绝大多数情况下,触发的时候循环已经结束了,所以循环相关的变量就是最后一次的取值。


要实现点击li,alert出li的索引值,需要将上面的代码进行以下的修改:

【原】理解javascript中的闭包(***********************************************)_回调函数

<ul id="test">     <li>第一个</li>     <li>第二个</li>     <li>第三个</li>     <li>第四个</li> </ul> var nodes=document.getElementsByTagName("li"); for(var i=0;i<nodes.length;i++){     (function(e){         nodes[i].onclick=function(){             alert(e);         };     })(i) }

【原】理解javascript中的闭包(***********************************************)_回调函数

解决思路: 增加若干个对应的闭包域空间(这里采用的是匿名函数),专门用来存储原先需要引用的内容(下标)。

当立即执行函数执行的时候,e 值不会被销毁,因为它的里面有个匿名函数(也可以说是因为闭包的存在,所以变量不会被销毁)。执行后,e 值 与全局变量 i 的联系就切断了,

也就是说,执行的时候,传进的 i 是多少,立即执行函数的 e 就是多少,但是 e 值不会消失,因为匿名函数的存在。


也可以用下面的解法,原理是一样的:

【原】理解javascript中的闭包(***********************************************)_回调函数

<ul id="test">     <li>第一个</li>     <li>第二个</li>     <li>第三个</li>     <li>第四个</li> </ul>  var nodes=document.getElementsByTagName('li'); for(var i = 0; i<nodes.length;i++){     (function(){        var temp = i;         nodes[i].onclick = function () {             alert(temp);         }     })(); }

【原】理解javascript中的闭包(***********************************************)_回调函数



​回到顶部​

注意事项


1、造成内存泄露

由于闭包会携带包含它的函数的作用域,因此会比其他函数占用更多的内存。过度使用闭包可能会导致内存占用过多,所以只有在绝对必要时再考虑使用闭包。

2、在闭包中使用this也可能会导致一些问题。

       代码示例:来源于《js高级程序设计3》;

其实我们的目的是想alert出object里面的name

【原】理解javascript中的闭包(***********************************************)_回调函数

var name="The Window";  var object={      name:"My Object",      getNameFunc:function(){          return function(){              return this.name;          }      }  }  alert(object.getNameFunc()()); // The Window

【原】理解javascript中的闭包(***********************************************)_回调函数

因为在全局函数中,this等于window,而当函数被作为某个对象的方法调用时,this等于那个对象。不过,匿名函数的执行环境具有全局性,因此其this对象通常指向window。

每个函数在被调用时,都会自动取的两个特殊变量:this和 arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止。也就是说,里面的return function只会搜索

到全局的this就停止继续搜索了。因为它永远不可能直接访问外部函数中的这两个变量。


稍作修改,把外部作用域中的this对象保存在一个闭包能够访问的变量里。这样就可以让闭包访问该对象了。

【原】理解javascript中的闭包(***********************************************)_回调函数

var name="The Window";  var object={      name:"My Object",      getNameFunc:function(){          var that=this;          return function(){              return that.name;          }      }  }  alert(object.getNameFunc()()); // My Object

【原】理解javascript中的闭包(***********************************************)_回调函数

我们把this对象赋值给了that变量。定义了闭包之后闭包也可以访问这个变量。因此,即使在函数返回之后,that也仍引用这object,所以调用object.getNameFunc()()就返回 “My Object”了。


​回到顶部​

总结


当在函数内部定义了其他函数,就创建了闭包。闭包有权访问包含函数内部的所有变量。

闭包的作用域包含着它自己的作用域、包含函数的作用域和全局作用域。

当函数返回一个闭包时,这个函数的作用域会一直在内存中保存到闭包不存在为止。

使用闭包必须维护额外的作用域,所有过度使用它们可能会占用大量的内存


有误之处,欢迎指出




首先,我们需要知道 JavaScript 里面的函数会创建内部词法作用域,是的,JavaScript 是词法作用域,也就是说作用域与作用域的层级关系在你书写的时候就已经确定了,而不是调用的时候,调用的时候确定的称为动态作用域,由于不是本篇文章的重点,就不再详细解释了,举两个例子自己领悟:

1 2 3 4 5 6 7 8 9 10

var name = 'fruit' function apple () { console.log(name) } function orange () { var name = 'orange' apple() } orange() // fruit


由于 JavaScript 是词法作用域,所以 ​​apple​​​ 函数的局部作用域的上层作用域是全局作用域,从书写的位置就看出来了。假设 JavaScript 是动态作用域,就要看函数的调用顺序了,由于 ​​apple​​​ 是在 ​​orange​​​ 中调用的,所以 ​​apple​​​ 的上层作用域是 ​​orange​​ 的局部作用域,那样的话会输出 orange!

这样的话,就制定了一套作用域访问的规则,这也是会有闭包的原因之一!

什么是闭包?


函数记住并访问其所在的词法作用域,叫做闭包现象,而此时函数对作用域的引用叫做闭包。


当我看到这句话的时候,泪流满面,国外的作者就是一语道破真相。简单的说,闭包就是引用,对谁的引用呢,对作用域的引用,只不过这种引用是有条件的——首先要记住作用域,然后再访问作用域!

什么叫记住作用域?

首先,我们都知道,在 JavaScript 里面,如果函数被调用过了,并且以后不会被用到,那么垃圾回收机制就会销毁由函数创建的作用域,我们还知道,对象(函数也是对象)的传递属于传引用,也就是类似于C语言里面的指针,并不会把真正的值拷贝给变量,而是把对象所在的位置传递给变量,所以,当函数被传引用到一个还未销毁的作用域的某个变量,由于变量存在,所以函数得存在,又因为函数的存在依赖于函数所在的词法作用域,所以函数所在的词法作用域也得存在,这样一来,就记住了该词法作用域。也就解释了该节的标题!下面举个例子说明一下:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

// 没有闭包现象的时候 function apple () { var count = 0 function output () { console.log(count) } fruit(output) } function fruit (arg) { console.log('fruit') } apple() // fruit


当我们在调用 ​​apple​​​ 的时候,本来 ​​apple​​​ 在执行完毕之后 ​​apple​​​ 的局部作用域就应该被销毁,但是由于 ​​fruit(output)​​​ 将 ​​output​​​ 传引用给了 ​​arg​​​,所以在 ​​fruit​​​ 执行的这段时间内,​​arg​​​ 肯定是存在的,被引用的函数 ​​output​​​ 也得存在,而 ​​output​​​ 依赖的 ​​apple​​ 函数产生的局部作用域也得存在,这就是所谓的“记住”,把作用域给记住了!

但是,上面的例子是闭包现象吗?不是,因为函数 ​​output​​​ 内部并没有访问记住的词法作用域的变量!在执行 ​​fruit(output)​​​ 的过程中,只发生了 ​​arg = output​​​ 的传引用赋值,而这个过程,只是把二者关联起来了,并没有去取 ​​arg​​​ 引用的对象的值,所以实际上也并没有访问 ​​output​​ 所在的词法作用域!

记住并访问

上面的代码,稍微修改一下就会产生闭包现象了:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

function apple () { var count = 0 function output () { console.log(count) } fruit(output) } function fruit (arg) { arg() } apple() // 0


现在,调用 ​​fruit​​​ 时,​​apple​​​ 的局部作用域处于“记住”的状态,这时候, ​​fruit​​​ 内部调用了 ​​arg()​​​,因为传引用,实际上访问并执行了 ​​apple​​​ 局部作用域的 ​​output​​​,不仅仅是这样,​​output​​​ 内部还访问了 ​​count​​​ 变量,这两次对 ​​apple​​ 局部作用域的引用都是闭包!

所以,之所以说所有回调函数的调用都会产生闭包现象,也是因为这个回调函数被传给了另外一个函数的参数,所以在另外一个函数的作用域消失之前,回调函数所在的词法作用域都被记住了,由于回调函数一定会被执行,所以回调函数所在的词法作用域至少被访问了一次,也就是至少访问回调函数本身,而这个对作用域的引用就是闭包。

闭包的作用

根据上面的讲解,估计你自己都能倒背如流了:


  1. 记住了函数所在的词法作用域,使其不被销毁;
  2. 能够访问函数所在词法作用域的变量;
  3. 创建模块(设计私有变量、公有函数等等)

还有很多,就不一一说了,下面就是利用闭包来解决一个常见的问题:

1 2 3 4 5 6

for (var i = 0; i < 5; i++) { // 为了方便说明,给函数起名叫 apple setTimeout(function apple () { console.log(i) // 5 个 5 }, 0) }


首先读者们先思考一个问题,这会产生闭包吗?

其实,上面也也会产生闭包,只不过 ​​apple​​​ 记住并访问的是全局作用域,为什么呢?因为回调函数被当做 ​​setTimeout​​​ 的参数传引用过去了,假设 ​​setTimeout​​ 实现如下

1 2 3 4

var setTimeout = function (callback, delay) { // 延迟 callback() }


看到没,因为 ​​setTimeout​​​ 属于异步函数,所以会等到 JS 执行完毕之后再调用 ​​callback​​​,所以这段时间 ​​callback​​​ 一直存在,所以函数 ​​apple​​​ 也一直存在,所以全局作用域并不会等 JavaScript 执行完毕后就销毁(函数 ​​apple​​​ 属于全局作用域的),这时候循环早结束了,所以 ​​i​​​ 也变成了 5,于是乎,这个时候 ​​apple​​ 对全局作用域的引用称为闭包!

上面也说了回调函数调用都会产生闭包,这里就当举例说明一下!

那么怎么解决以上问题呢,很简单,让回调函数记住不同的作用域就行了!

1 2 3 4 5 6 7 8

for (var i = 0; i < 5; i++) { // 为了方便说明,给函数起名叫 apple (function baz (i) { setTimeout(function apple () { console.log(i) }, 0) })(i) // 0 1 2 3 4 }

上面用立即执行函数解决了问题,因为函数有局部作用域,所以调用 5 次函数会产生 5 个局部作用域,每个作用域的 ​​i​​​ 由各次循环的 ​​i​​​ 传递赋值,而每个作用域内都存在 ​​apple​​​ ,都记住了各自的作用域,也就取到了不同的 ​​i​​!

不过通常来说,闭包都是按以下方式产生:

1 2 3 4 5 6 7 8 9 10 11 12

function apple () { var name = 'apple' var output = function () { console.log(name) } return output } var out = apple() out() // apple


上述将函数传引用给了全局作用域的变量,显然,闭包(对 ​​apple​​​ 作用域的引用)在全局作用域都存在的情况下都可能发生,而且后面也执行了 ​​out()​​!

更常见的写法是下面这种:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

function Apple () { var name = 'apple' var output = function () { console.log(name) } var setName = function (arg) { name = arg } return { output: output, setName: setName } } var apple = Apple() apple.output() // apple apple.setName('Apple') apple.output() // Apple


这就是模块的一个例子,​​name​​ 通常被称为私有变量!

结语

闭包没什么了不起的,这是被人玩的过于玄乎,其实这是人们很自然的想法:我在别的地方调用函数,总得保持函数正常运行吧!“闭包”这种机制很轻松的帮你解决了这个问题,我们不必搞懂闭包是什么也经常在实现它(如果这句话写在前面,会不会很多人都不看了,哈哈),这是语言设计者的过人之处,但是,你不搞懂它,总被人质疑:你不懂闭包吧!实际上,我们都实现了很多次闭包,所以,你把内部机制详细搞清楚了,就不会再害怕别人的质疑了,哈哈!当然,如果你喜欢钻研,更有必要了解其中的机制了,体会到寻找语言设计者设计思路的快感!

最后,再总结一下:函数记住并访问其所在的词法作用域,叫做闭包现象,而此时函数对作用域的引用叫做闭包。

最后的最后,再强调一下:闭包就是引用!