关于闭包,目前有以下说法:html
根据排列顺序也能够看出,我我的对这些说法的认同程度。其实你们说的都是同一个东西,只是描述是否精确的问题。
为了充分理解以上的说法,要先理解一些术语:前端
简单来讲,词法做用域就是:根据变量定义时所处的位置,来肯定变量的做用范围。(词法解析,经过阅读包含变量定义在内的数行源码就能知道变量的做用域)
举例而言,定义在全局的变量,它的做用范围是全局的,因此被称为全局变量;定义在函数内部的变量,它的做用范围是局部的,因此被称为局部变量。chrome
函数在建立时,会同时保存它的做用域链。——这个保存的做用域链包含了该函数所处的做用域对象的集合。由于全部函数都在全局做用域下声明,因此这个保存的做用域链必定包含全局做用域对象(global)。此外,若是函数是在其余函数内部声明的,那它保存的做用域链中除了global以外,还包含它建立时所处的局部做用域对象。(在chrome中直接标识为closure,在firefox中则标识为块)。显然,这个做用域链其实是一个指向做用域对象集合的指针列表。浏览器
函数在执行时,会建立一个执行环境、执行时做用域链以及活动对象。——活动对象(activation object)是指当前做用域对象(处于活动状态的,它包含arguments、this以及全部局部变量)。执行时做用域链其实是函数建立时保存的做用域链的一个复制,但它更长,由于活动对象被推入了执行时做用域链的前端。每次函数在执行时都会建立一个新的执行环境(execution context),它对应着一个全新的执行时做用域链。闭包
根据JavaScript的垃圾回收机制:通常状况下,函数在执行完毕后,执行环境(包括执行时做用域链)将自动被销毁,占用的内存将被释放。函数
JavaScript 是一门具备自动垃圾回收机制的语言。
这种机制的原理是找出那些再也不继续使用的变量,而后释放其占用的内存。目前,找出再也不继续使用的变量的策略有两种:标记清除(主流浏览器)和引用计数(IE8及如下)。
标记清除:垃圾收集器在运行的时候会给存储在内存中的全部变量都加上标记;而后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记;最后,垃圾收集器销毁那些带标记的值并回收它们所占用的内存空间。垃圾收集器会按照固定的时间间隔周期性地执行这一操做。
引用计数:当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是 1。若是同一个值又被赋给另外一个变量,则该值的引用次数加 1。相反,若是包含对这个值引用的变量又取得了另一个值,则这个值的引用次数减 1。当这个值的引用次数变成 0 时,则说明没有办法再访问这个值了,于是就能够将其占用的内存空间回收回来。这样,当垃圾收集器下次再运行时,它就会释放那些引用次数为零的值所占用的内存。(引用计数的失败之处在于它没法处理循环引用)性能
如今,什么是闭包呢?
——“闭包是函数和声明该函数的词法环境的组合”(MDN)this
function a(){ console.log('1'); } a();
以上例子:函数a,和它建立时所在的全局做用域,构成一个闭包。因而有人说每一个函数实际上都是一个闭包,但准确来说,应该是每一个函数和它建立时所处的做用域构成一个闭包。
但这个闭包叫什么名字呢?
在chrome和firefox调试中,将函数a所在做用域的名字,做为闭包的名字;在JavaScript高级程序设计中则将函数a的名字,做为闭包的名字。这样一来,每一个函数都是一个闭包的说法彷佛又“准确”了一些。
其实咱们书写的全部js代码,都处在全局做用域这个大大的闭包之中,只是咱们意识不到它做为一个闭包存在着。spa
function a(){ var b = 1; function c(){ console.log(b); } return c } var d = a(); d(); // 1
以上例子:除了函数a和全局做用域构成一个闭包之外,函数c和局部做用域(函数a的做用域)也构成一个闭包。
先不关注这些函数内部的逻辑,咱们只看结构:
函数a声明了,而后在var d = a();
这一句执行。经过以上对词法做用域、做用域链以及垃圾回收机制的理解,咱们能够得出如下结论:
函数a在声明时保存了一个做用域链,在它执行时又建立了一个执行环境(以及执行时做用域链)。通常状况下,当函数a执行完毕,它的执行环境将被销毁。但在这个例子里,函数a中的变量c,被return
突破做用域的限制赋值给了变量d,而变量c是一个函数,它使用了它建立时所处的做用域(函数a的做用域)中的变量b,这意味着,在函数d执行完毕以前,函数c以及它建立时所处的做用域中变量(变量b)不能够被销毁。
这打断了函数a执行环境的销毁进程,它被保存了下来,以备函数d调用时使用。看看被保存的是什么?一个函数c和它建立时所在的做用域。一个闭包。firefox
function a(){ var b = 1; function c(){ b++; console.log(b); } return c } var d = a(); d(); // 2 d(); // 3 var e = a(); e(); // 2 e(); // 3
以上例子,函数a被执行了两次并分别赋值给了d、e,显然,函数a的两次执行建立了两个执行环境,它们本该被销毁,但因为函数c的存在(有权访问另外一个函数内部变量的函数),它们被保存下来。函数d的两次执行,使用同一个执行环境中的变量b,因此b递增了;因为函数e使用的是另外一个执行环境中的变量b,因此它从新开始递增。
因此,什么是闭包呢?
闭包是一个函数和它建立时所在做用域的组合。在咱们平常应用中,一般是将一个函数定义在另外一个函数的内部并从中返回,以使它成为一个在函数外部仍有权限访问函数内部做用域的函数。
jQuery就是定义在一个匿名自执行函数内部的函数,当它被赋值给全局做用域变量$
和jQuery
时,在全局做用域使用$
和jQuery
方法,就可以访问到那个匿名自执行函数的内部做用域(其中包含的变量等)。在jQuery这个例子中,内部函数jQuery和其所在的匿名自执行函数做用域就构成一个闭包。
一个经典的例子:
// html <ul><li></li><li></li><li></li></ul> var lis = document.querySelector('ul').children; for (var i = 0; i < lis.length; i++) { lis[i].addEventListener('click', function(){ console.log(i); }) } var event = document.createEvent('MouseEvent'); event.initEvent('click', false, false); for (var j = 0; j < lis.length; j++) { lis[j].dispatchEvent(event); }
为页面上的全部li标签绑定点击函数,点击后输出自身的序号。在以上例子中,显然将输出 3, 3, 3;而非 0, 1, 2;
一个通俗的解释是,当点击li标签时,for循环已经执行完毕,i的值已经肯定。因此三个li标签点击输出同一个i的值。
咱们稍微改动一下代码:
// html <ul><li></li><li></li><li></li></ul> var lis = document.querySelector('ul').children; for (var i = 0; i < lis.length; i++) { (function(i){ lis[i].addEventListener('click', function(){ console.log(i); }) })(i); } var event = document.createEvent('MouseEvent'); event.initEvent('click', false, false); for (var j = 0; j < lis.length; j++) { lis[j].dispatchEvent(event); }
以上例子,当点击li标签时,for循环已经执行完毕,i的值已经肯定,可为何结果会输出 0, 1, 2 呢?
实际上,这是闭包在做怪:
click
事件的匿名函数 跟外层自执行匿名函数的做用域构成了一个闭包。在循环中,外层匿名自执行函数本该在执行结束后销毁它的执行环境,释放其内存,但因为它的参数(变量)i 还被事件监听函数引用着,因此这个执行环境没法被销毁,它将被保存着。每一次的循环,匿名自执行函数都将执行一次,并保存一个执行环境;当循环结束,相似的执行环境共有三个,每个里面的变量i的值都是不一样的。
回到第一个例子,匿名事件函数实际上和声明它的全局做用域也构成了一个闭包,但在三次循环中,i 都不曾离开这个闭包,它一直递增直至3,三个点击事件函数引用同一个执行环境中的变量i,它们的值必然是相同的。
离开闭包的泥淖,给这个例子一个较为合理的写法:
// html <ul><li></li><li></li><li></li></ul> var lis = document.querySelector('ul').children; var say = function(){ console.log(this.index); } for (var i = 0; i < lis.length; i++) { lis[i].index = i; lis[i].addEventListener('click', say); } var event = document.createEvent('MouseEvent'); event.initEvent('click', false, false); for (var j = 0; j < lis.length; j++) { lis[j].dispatchEvent(event); }
总结:理解闭包的概念是重要的,但咱们不该当过多的使用闭包,它有优势,也优缺点,是一把双刃剑。使用闭包能够建立一个封闭的环境,使得咱们能够保存私有变量,避免全局做用域命名冲突,增强了封装性;但它常驻内存的特性也对网页的性能形成了比较大的影响,在引用计数的垃圾回收策略下更容易形成内存泄漏。