资讯专栏INFORMATION COLUMN

深入理解闭包的概念

anyway / 1337人阅读

摘要:离开闭包的泥淖,给这个例子一个较为合理的写法总结理解闭包的概念是重要的,但我们不应当过多的使用闭包,它有优点,也优缺点,是一把双刃剑。

闭包

关于闭包,目前有如下说法:

闭包是函数和声明该函数的词法环境的组合(MDN)

函数对象可以通过作用域链相互关联起来,函数体内部的变量都可以保存在函数作用域内。这种特性在计算机科学文献中被称为闭包(JavaScript权威指南)

闭包,指的是词法表示包括不被计算的变量的函数,也就是说,函数可以使用函数之外定义的变量(W3school)

闭包是指有权访问另一个函数作用域中的变量的函数(JavaScript高级程序设计)

根据排列顺序也可以看出,我个人对这些说法的认同程度。其实大家说的都是同一个东西,只是描述是否精确的问题。
为了充分理解以上的说法,要先理解一些术语:

词法作用域

简单来说,词法作用域就是:根据变量定义时所处的位置,来确定变量的作用范围。(词法解析,通过阅读包含变量定义在内的数行源码就能知道变量的作用域)
举例而言,定义在全局的变量,它的作用范围是全局的,所以被称为全局变量;定义在函数内部的变量,它的作用范围是局部的,所以被称为局部变量。

作用域链

函数在创建时,会同时保存它的作用域链。——这个保存的作用域链包含了该函数所处的作用域对象的集合。因为所有函数都在全局作用域下声明,所以这个保存的作用域链一定包含全局作用域对象(global)。此外,如果函数是在其他函数内部声明的,那它保存的作用域链中除了global之外,还包含它创建时所处的局部作用域对象。(在chrome中直接标识为closure,在firefox中则标识为块)。显然,这个作用域链实际上是一个指向作用域对象集合的指针列表

函数在执行时,会创建一个执行环境、执行时作用域链以及活动对象。——活动对象(activation object)是指当前作用域对象(处于活动状态的,它包含arguments、this以及所有局部变量)。执行时作用域链实际上是函数创建时保存的作用域链的一个复制,但它更长,因为活动对象被推入了执行时作用域链的前端。每次函数在执行时都会创建一个新的执行环境(execution context),它对应着一个全新的执行时作用域链。

根据JavaScript的垃圾回收机制:一般情况下,函数在执行完毕后,执行环境(包括执行时作用域链)将自动被销毁,占用的内存将被释放。

垃圾回收机制

JavaScript 是一门具有自动垃圾回收机制的语言。
这种机制的原理是找出那些不再继续使用的变量,然后释放其占用的内存。目前,找出不再继续使用的变量的策略有两种:标记清除(主流浏览器)和引用计数(IE8及以下)。
标记清除:垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记;然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记;最后,垃圾收集器销毁那些带标记的值并回收它们所占用的内存空间。垃圾收集器会按照固定的时间间隔周期性地执行这一操作。
引用计数:当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是 1。如果同一个值又被赋给另一个变量,则该值的引用次数加 1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减 1。当这个值的引用次数变成 0 时,则说明没有办法再访问这个值了,因而就可以将其占用的内存空间回收回来。这样,当垃圾收集器下次再运行时,它就会释放那些引用次数为零的值所占用的内存。(引用计数的失败之处在于它无法处理循环引用)

现在,什么是闭包呢?
——“闭包是函数和声明该函数的词法环境的组合”(MDN)

function a(){
  console.log("1");
}
a();

以上例子:函数a,和它创建时所在的全局作用域,构成一个闭包。于是有人说每个函数实际上都是一个闭包,但准确来讲,应该是每个函数和它创建时所处的作用域构成一个闭包。
但这个闭包叫什么名字呢?
在chrome和firefox调试中,将函数a所在作用域的名字,作为闭包的名字;在JavaScript高级程序设计中则将函数a的名字,作为闭包的名字。这样一来,每个函数都是一个闭包的说法似乎又“准确”了一些。
其实我们书写的所有js代码,都处在全局作用域这个大大的闭包之中,只是我们意识不到它作为一个闭包存在着。

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和它创建时所在的作用域。一个闭包。

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

总结:理解闭包的概念是重要的,但我们不应当过多的使用闭包,它有优点,也优缺点,是一把双刃剑。使用闭包可以创建一个封闭的环境,使得我们可以保存私有变量,避免全局作用域命名冲突,加强了封装性;但它常驻内存的特性也对网页的性能造成了比较大的影响,在引用计数的垃圾回收策略下更容易造成内存泄漏。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/100191.html

相关文章

  • JavaScript中闭包

    摘要:闭包引起的内存泄漏总结从理论的角度将由于作用域链的特性中所有函数都是闭包但是从应用的角度来说只有当函数以返回值返回或者当函数以参数形式使用或者当函数中自由变量在函数外被引用时才能成为明确意义上的闭包。 文章同步到github js的闭包概念几乎是任何面试官都会问的问题,最近把闭包这块的概念梳理了一下,记录成以下文章。 什么是闭包 我先列出一些官方及经典书籍等书中给出的概念,这些概念虽然...

    HmyBmny 评论0 收藏0
  • 说说Python中闭包 - Closure

    摘要:闭包可以用来在一个函数与一组私有变量之间创建关联关系。夹带私货外部变量返回的是函数,带私货的函数支持将函数当成对象使用的编程语言,一般都支持闭包。所以说当你的装饰器需要自定义参数时,一般都会形成闭包。 Python中的闭包不是一个一说就能明白的概念,但是随着你往学习的深入,无论如何你都需要去了解这么一个东西。 闭包的概念 我们尝试从概念上去理解一下闭包。 在一些语言中,在函数中可以(嵌...

    leon 评论0 收藏0
  • 深入贯彻闭包思想,全面理解JS闭包形成过程

    摘要:下面我们就罗列闭包的几个常见问题,从回答问题的角度来理解和定义你们心中的闭包。函数可以通过作用域链相互关联起来,函数内部的变量可以保存在其他函数作用域内,这种特性在计算机科学文献中称为闭包。 写这篇文章之前,我对闭包的概念及原理模糊不清,一直以来都是以通俗的外层函数包裹内层....来欺骗自己。并没有说这种说法的对与错,我只是不想拥有从众心理或者也可以说如果我们说出更好更低层的东西,逼格...

    snowell 评论0 收藏0
  • 深入理解JavaScript(二):由一道题来思考闭包

    摘要:中所有的事件绑定都是异步编程当前这件事件没有彻底完成,不再等待,继续执行下面的任务当绑定事件后,不需要等待执行,继续执行下一个循环任务,所以当我们点击执行方法的时候,循环早已结束即是最后。 概念 闭包就是指有权访问另一个函数作用域中的变量的函数 点击li标签弹出对应数字 0 1...

    曹金海 评论0 收藏0

发表评论

0条评论

anyway

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<