JavaScript闭包的那些事~
<h2 id="javascript闭包">JavaScript闭包</h2><h3 id="1函数在javascript中的地位">1.函数在JavaScript中的地位</h3>
<blockquote>
<p>在介绍闭包之前,可以先聊聊函数在JavaScript中的地位,因为闭包的存在是与函数息息相关的。</p>
</blockquote>
<ul>
<li>JavaScript之所以可以称之为支持<strong>头等函数</strong>的编程语言,是因为JavaScript中函数是<strong>一等公民</strong>;</li>
<li>函数不仅在JavaScript中扮演着重要的角色,而且可以使用的非常灵活;</li>
<li>函数不仅可以作为另一个函数的<strong>参数</strong>,也可以作为另一个函数的<strong>返回值</strong>;</li>
<li>这样使用的函数也称之为<strong>高阶函数</strong>,像JS的数组中就实现了许多高阶函数(map、filter、reduce等);</li>
</ul>
<h3 id="2javascript中闭包的定义">2.JavaScript中闭包的定义</h3>
<blockquote>
<p>闭包的概念出现于60年代,最早实现闭包的程序是Scheme,那么就可以理解为什么JavaScript中有闭包了,因为JavaScript中大量的设计是源自于Scheme的。而在不同的地方对JavaScript闭包的定义是不一样的,但是整体核心还是一致的,只是用不同的话来描述JavaScript闭包,以下是摘抄自三个地方的定义。</p>
</blockquote>
<p>维基百科中对闭包的定义:</p>
<ul>
<li>闭包(Closure),又称<strong>词法闭包</strong>(Lexical Closure)或<strong>函数闭包</strong>(function closures),是在支持<strong>头等函数</strong>的编程语言中,实现词法绑定的一种技术;</li>
<li>闭包在实现上是一个<strong>结构体</strong>,它存储了<strong>一个函数</strong>和<strong>一个关联的环境</strong>(相当于一个符号查找表);</li>
<li>闭包跟函数最大的区别在于,当捕捉闭包的时候,它的<strong>自由变量</strong>会在捕捉时被确定,这样即使脱离了捕捉时的上下文,它也能照常运行;</li>
</ul>
<p>MDN中对闭包定义:</p>
<ul>
<li>一个<strong>函数</strong>和对其<strong>周围状态</strong>(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure);</li>
<li>也就是说,闭包让你可以<strong>在一个内层函数中访问到其外层函数的作用域</strong>;</li>
<li>在JavaScript中,每当创建一个函数,闭包就会在函数创建的同时被创建出来;</li>
</ul>
<p>《JavaScript高级程序设计》中对闭包的定义:</p>
<ul>
<li>闭包指的是那些引用了<strong>另一个函数作用域中变量</strong>的函数,通常是在<strong>嵌套函数</strong>中实现的;</li>
</ul>
<p>对闭包定义的总结:</p>
<ul>
<li>以上对闭包的三种定义,都提到了函数、环境、作用域和变量,总结为就是:一个<strong>函数</strong>,如果它可以访问<strong>外层作用域</strong>的<strong>自由变量</strong>,那么这个函数就是一个闭包;</li>
<li>闭包由两部分组成:<strong>内层函数+可以访问的外层自由变量</strong>;</li>
<li>广义角度:JavaScript中的函数都是闭包(都可以形成闭包);</li>
<li>狭义角度:JavaScript中的一个函数,如果访问了外层作用域的变量,那么它是一个闭包;</li>
</ul>
<h3 id="3闭包是如何形成的">3.闭包是如何形成的?</h3>
<blockquote>
<p>看了一大堆闭包的定义,那么到底什么情况下就形成了闭包呢?</p>
</blockquote>
<p>(1)<strong>产生闭包的条件</strong>:简单来说,满足以下几个条件就可以说产生了闭包。</p>
<ul>
<li>函数嵌套;</li>
<li>内层函数引用了外层函数作用域中的变量;</li>
<li>外层函数执行;</li>
</ul>
<p>(2)<strong>常见的闭包。</strong></p>
<ul>
<li>
<p>将一个函数作为另一个函数返回值,例如:</p>
<pre><code class="language-js">function foo() {
var name = 'foo'
return function bar() {
console.log(name)
}
}
var fn = foo()
fn()
</code></pre>
</li>
<li>
<p>将一个函数作为实参传递另一个函数,例如:</p>
<pre><code class="language-js">function showDelay(msg) {
setTimeout(function() {
console.log(msg)
})
}
showDelay('我形成了闭包')
</code></pre>
</li>
</ul>
<h3 id="4闭包的访问和执行过程">4.闭包的访问和执行过程</h3>
<blockquote>
<p>下面介绍闭包在访问和执行过程中的内存表现,进一步深入对闭包的了解,以如下代码为例:</p>
</blockquote>
<p>示例代码:</p>
<pre><code class="language-js">function foo() {
var name = 'foo'
return function bar() {
console.log(name)
}
}
var fn = foo()
fn()
</code></pre>
<ul>
<li>
<p>首先,在执行全局代码之前,会在内存中创建一个全局对象(GO),将全局执行上下文压入栈中,这时的fn还未被赋值;</p>
<p><img src="https://img2022.cnblogs.com/blog/2506425/202202/2506425-20220209233615003-2119836539.png" alt="" loading="lazy"></p>
</li>
<li>
<p>当执行到<code>var fn = foo()</code>时,在调用foo之前创建foo的活动对象(AO),创建foo函数执行上下文,并将其压入栈中,接着执行foo函数,执行完成后fn指向bar函数内存地址;</p>
<p><img src="https://img2022.cnblogs.com/blog/2506425/202202/2506425-20220209233635202-1292839195.png" alt="" loading="lazy"></p>
</li>
<li>
<p>foo函数执行完成后,foo函数执行上下文会弹出栈,而按道理foo的活动对象(AO)是需要被销毁的,那到底有没有销毁,我们接着看;</p>
</li>
<li>
<p>接着执行<code>fn()</code>,因为fn是指向bar函数的,执行之前会先创建bar的活动对象(AO),然后执行<code>console.log(name)</code>,而name会先去自己的AO中查找,发现没有找到就会去到上层作用域(父级作用域)中查找,最终找到<code>foo</code>并打印,这里bar函数的上层作用域就是foo函数的作用域对应foo的活动对象(AO);</p>
<p><img src="https://img2022.cnblogs.com/blog/2506425/202202/2506425-20220209233648310-1492404869.png" alt="" loading="lazy"></p>
</li>
<li>
<p>bar函数执行完成后,bar函数的执行上下文弹出栈,对应bar的活跃对象(AO)被销毁,而foo的活跃对象(AO)还一直存留在内存中;</p>
<p><img src="https://img2022.cnblogs.com/blog/2506425/202202/2506425-20220209233701785-320321084.png" alt="" loading="lazy"></p>
</li>
<li>
<p>但是bar函数的父级作用域是在什么时候确定的呢?</p>
<ul>
<li>在编译bar函数时就已经确定了bar函数的父级作用域——foo的活动对象(AO)早在编译时就加入到了bar函数的作用域链中;</li>
<li>为什么执行完foo函数后还可以访问其name变量,就可以回答上面的问题了,foo的活动对象(AO)是没有被销毁的;</li>
<li>因为bar函数的作用域链中依然对foo的活动对象(AO)有引用,导致其不能正常销毁;</li>
</ul>
</li>
</ul>
<p>根据上面闭包的访问和执行过程结合闭包的定义做一个总结:</p>
<ul>
<li>
<p>在维基百科中定义的“闭包在实现上是一个结构体,它存储了一个函数和一个关联的环境(相当于一个符号查找表)”,以及MDN中定义的“一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)”,其函数和关联的环境、函数和对其周围状态的引用,对应的就是上面的bar函数和上层作用域中的name;</p>
<p><img src="https://img2022.cnblogs.com/blog/2506425/202202/2506425-20220209233713392-1174567297.png" alt="" loading="lazy"></p>
</li>
<li>
<p>而对于维基百科中提到的“闭包跟函数最大的区别在于,当捕捉闭包的时候,它的<strong>自由变量</strong>会在捕捉时被确定,这样即使脱离了捕捉时的上下文,它也能照常运行”,也就是在捕捉bar函数时,同时捕捉到对name这个自由变量的引用,执行完foo函数后,将bar函数赋值给fn,最后执行fn时,也是能正常访问到name的;</p>
</li>
<li>
<p>了解了其访问执行过程后,可以发现本应该被销毁的foo的活跃对象(AO),在代码执行完后最终没能被销毁,而这样的情况称之为<strong>内存泄露</strong>,下面就来谈谈闭包的内存泄露;</p>
</li>
<li>
<p>如果对上面的执行过程不清楚,可以先看看这篇文章:JavaScript的执行过程(深入执行上下文、GO、AO、VO和VE等概念)</p>
</li>
</ul>
<h3 id="5闭包的内存泄露">5.闭包的内存泄露</h3>
<blockquote>
<p>闭包会保留它们包含函数的作用域,所以比其它函数更占用内存,而过渡使用闭包可能导致内存过度占用,也就是内存泄露,而除了内存泄露这个概念还有一个内存溢出的概念,下面就先了解一下这两者的区别和关系:</p>
</blockquote>
<ul>
<li>内存溢出:程序运行出现错误,当程序运行需要的内存超过了剩余的内存时,就会抛出内存溢出的错误(比如,死循环)。</li>
<li>内存泄露:占用的内存没有及时释放,内存泄露积累过多就容易导致内存溢出(比如,意外的全局变量、没有及时清理计时器或回调、<strong>闭包</strong>)</li>
</ul>
<p>那具体怎么解决闭包产生的内存泄露呢?</p>
<ul>
<li>针对于上面的代码,可以在最后执行<code>fn = null</code>;</li>
<li>因为将fn设置为null时,就不再对bar函数有引用,bar函数失去了全部的引用就会被销毁,对应foo的活动对象(AO)也就失去了引用,在下一次的垃圾回收(GC)检测中,就会被销毁掉;</li>
</ul>
<h3 id="6使用浏览器查看闭包">6.使用浏览器查看闭包</h3>
<blockquote>
<p>闭包其实是可以在浏览器中观察到的,在查看闭包之前先来讨论一个问题,外层函数的活跃对象(AO)不会被销毁,是不是里面所有的属性都不会被销毁呢?</p>
</blockquote>
<p>如果将上面的代码改成下面这样,多增加两个变量age和message,但是bar函数中并没有对age和message有引用:</p>
<pre><code class="language-js">function foo() {
var name = 'foo'
var age = 18
var message = 'hello bibao'
return function bar() {
console.log(name)
}
}
var fn = foo()
fn()
</code></pre>
<ul>
<li>
<p>形成闭包后,name是一定不会被销毁的,这个上面已经验证过了;</p>
</li>
<li>
<p>具体age和message有没有被销毁,可以在代码中打上断点,在Chrome浏览器查看对应的闭包;</p>
<p><img src="https://img2022.cnblogs.com/blog/2506425/202202/2506425-20220209233729604-1089780171.png" alt="" loading="lazy"></p>
</li>
<li>
<p>观察上面的结果是没有age和message属性的,这个就涉及到JS引擎的实现了,像V8引擎就对其进行了优化,对于闭包内层函数没有使用到的自由变量,是不会被保存的,这样就大大提升了内存的使用率;</p>
</li>
</ul><br><br>
来源:https://www.cnblogs.com/MomentYY/p/15877394.html
頁:
[1]