Lasy

前端开发攻城狮~

JavaScript闭包

JavaScript闭包

闭包一直被认为是JavaScript的一个难点,我也是浏览了很多文章之后才弄懂闭包是怎么一回事。网上大多文章对闭包的理解都比较片面,我打算从闭包的定义上入手,对闭包进行解释,可能会和很多人理解的闭包不太一样,但是并不难理解。

概念

这是Wiki上的定义:

In programming languages, a closure (also lexical closure or function closure) is a technique for implementing lexically scoped name binding in a language with first-class functions. Operationally, a closure is a record storing a function together with an environment.

这是MDN上的定义:

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

这里是阮一峰大佬的理解:

闭包就是能够读取其他函数内部变量的函数。

阮一峰大佬的理解通俗易懂,而且很多人也的确是这么理解的。我个人认为这种理解方式并不全面,我还是更倾向于wiki和MDN上的定义,虽然比较抽象,但是若要把定义理解了,那对于闭包掌握得应该更透彻。

对于闭包概念的理解

首先对概念做出解释: 闭包是函数和声明该函数的词法环境的组合。有两个关键点,

  • 一个是函数,这个想必不必解释;
  • 另一个是该函数的词法环境,该函数的词法环境指的是 该函数对于该函数所需要的不在该函数作用域中的变量的映射或者引用。

这两者的组合就是闭包。 网上给的很多例子都是比较复杂的闭包,用来理解比较难而且很容易让人混淆闭包的概念。 闭包的概念乍很抽象,但其实也不难理解。闭包是函数和声明该函数的词法环境的组合,也就是说有个函数,有这个函数的词法环境就是闭包了呗?没错,看以下例子:

这就是闭包。printA()是函数,printA()需要的词法环境是变量a。准备来说,printA()和变量a的引用的组合就是一个闭包。 把以上例子改一下:

这依然是闭包。printA()是函数,printA()并不需要它的作用域以外的变量,所以它的词法环境中并没有变量。 那么,这么说的话,是不是所有函数都是闭包? 是的,准确说,所有函数都会产生一个闭包。闭包是一个函数和词法环境的组合,准确来讲不能说一个函数是闭包,但是一个函数必然和一个闭包相关联。
所有函数都会关联一个闭包。
是不是觉得闭包概念的理解很简单,当然,就是这么简单,闭包的概念并没有那么难。 ## 闭包的特性
闭包的词法环境中的变量不会被垃圾回收机制回收,也就是说,闭包一旦创建闭包的词法环境中的变量就会常驻内存,除非所有包含它的闭包中的函数都被销毁。

来看一个例子:

这里用闭包的概念来理解就是:函数f1和它的词法环境(并没有变量)的组合是一个闭包,函数f2和变量n的引用是另一个闭包。 然后我们来分析以下为什么第二次执行result()结果是3: 首先,result是对函数f2的引用(f1()返回的结果就是函数f2),函数f2和变量n是一个闭包,所以由闭包的特性可以知道,只要f2没被销毁变量n就会一直在内存当中,所以,每次执行result()返回的都是内存中同一个n的值。
再来一个例子:

这是阮一峰博客中的例子,首先来分析一下闭包。 这里一共有三个函数,也就是会产生三个闭包,而我们需要分析的是nAdd和f2相关的闭包。 nAdd关联的闭包的词法环境中变量是n,f2关联的闭包词法环境也是n。也就是说,函数nAdd和f2共享了同一个词法环境,再结合闭包的特性,也就不难理解这段代码的输出结果了。
result是函数f2的引用;nAdd由于没有加var,所以nAdd是全局变量(相当于在函数定义之前定义了var nAdd)。显然,result()和nAdd()操作的是同一个变量n,且由于nAdd和f2都没有被销毁,所以这个n一直存在于内存当中。因此第一次result()结果为999,nAdd()将内存中的n变量值加1,再次result(),结果为1000。
回顾一下闭包的特性:闭包的词法环境中的变量不会被垃圾回收机制回收,也就是说,闭包一旦创建闭包的词法环境中的变量就会常驻内存,除非所有包含它的闭包中的函数都被销毁。 换言之,销毁闭包中的函数,与它相关的词法环境中的变量并不一定会销毁。举例说明:

add, sub, getResult的闭包中同时包含了变量n,因此它们共享这个变量。 我们通过result=null来将getResult函数销毁,此时执行sub(), add()操作的依然是内存中的n,说明n并没有被销毁。若要销毁n,则需要在此基础上增加
sub=null;add=null,即将n所在的所有闭包中的函数销毁。 ## 闭包生成的时机
当函被声明的时候,就会形成一个闭包。
用例子来说明:

这是MDN上的例子。 function(y) { return x + y; }函数被声明的时候,它的闭包形成,词法环境为x的引用。 调用add5(2)的时候,x的值为5,所以最后执行结果是5+2=7。 当调用add10(2)的时候,x的值是10。所以最后执行的结果是10+2=12。

更多例子

例1-继续分析阮一峰大佬的例子

思考1

思考2

思考1和思考2的区别就是思考2将this用变量that保存了下来。我们来分析以下。 思考1:getNameFunc将匿名函数function(){ return this.name;}作为了返回值,也就是说,getNameFunc的闭包就是该匿名函数的闭包。要得出结果,只需要分析这个匿名函数的词法环境。显然,这个匿名函数内并没有用到它的作用域以外的变量,所以词法环境中没有变量。最后执行object.getNameFunc()的时候也就是相当于执行匿名函数,而匿名函数中this指向window,所以最终返回的是window.name,也就是”The
window”。 思考2:同样地,getNameFunc的闭包就是匿名函数的闭包。但是不同的是,该匿名函数的闭包的词法环境中有一个变量that,而that保存的是当时的this,而这个this指向的是object,所以最后返回的是object.name,也就是”My
Object”。 上面这两个思考题难度并不在闭包上,而是在this的指向和作用域上。

例2-MDN上用闭包模拟私有方法的一个例子(模块模式)

这个例子中increment, decrement, value三个函数共享相同的词法环境,且由于这三个函数的闭包,privateCounter会一直存在于内存当中。

例3-循环闭包问题(循环中的事件绑定)

这是每个前端工程师都遇到过的问题。 这样的结果是:每个点击都输出 3。 原因: 三次循环分别给三个li的onclick事件绑定了一个函数,这三个函数分别和i形成了闭包,三个函数共享词法环境。 三个函数的输出取决于i,词法环境中只有一个i,准确来说是i的引用并不是当时i的值,所以三个函数必然输出相同的值,这个输出的值则取决于i在onclick事件触发的时候的值。
onclick事件触发时,循环肯定早就结束了,i的值是3,所以输出都是3。

解决方案:

  1. IIFE匿名闭包

IIFE为函数内部创建了独立的作用域,通过参数的形式将i值传入,由于IIFE是立即调用的,所以传入的i值即是每次循环的i值。 这时候onclick后面的函数的闭包中的变量则是IIFE中的index(即传入的i值),而不是for循环中i,所以可以得出正确结果。
2. 通过更多的闭包来解决

clickFunc函数返回的是一个匿名函数 function () { doSomething(i); },这个匿名函数的闭包中并没有i,因为i值是通过参数传递过来的值,而不是引用,所以i值也是每次循环的i值,故得出正确结果。
其实,从另一种角度上讲,这两种解决方案是相同的——通过函数参数传递值而不是通过闭包。众所周知,JS中函数参数传递的是值,所以是“实时”的,而闭包中是引用,所以会有“延时”现象。
3. 通过this关键字来解决

这是一种比较讨巧的做法。为每一个li添加了一个index属性来保存i值,在onclick触发的事件中,通过this.index读出i值,得出正确结果。
4. 用let替换var

这是最容易实现的方法。 JS没有块级作用域,JS变量的作用域是一个函数体,所以for循环中的i的作用域并不是两个花括号之间,在for循环之外依然可以访问到i,所以for循环的每次循环都是为i赋一个新的值,这就导致三个函数闭包中保存都是同一个i的引用。
let声明的变量具有块级作用域,所以for循环之外是访问不到i的,所以每次for的循环都要重新创建一个i,因此三个函数的闭包中保存了不同的i的引用,故得到正确的值。

参考链接:

点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注