1.什么是闭包?
在学习什么是闭包之前我们先来看看闭包是怎么出现的?
闭包的概念出现与60年代,最早实现闭包的程序是Scheme而又因为JavaScript中有大量的设计是来源于Scheme的,因此闭包这个概念也就被JavaScript引入了进来。 在计算机科学(维基百科)中闭包是这么定义的:
- 闭包,又称词法闭包(Lexical Closure)或者函数闭包(function Closure);
- 是在支持头等函数的编程语言中,实现词法绑定的一种技术;
- 闭包在实现上是一个结构体,它存储了一个函数和一个关联的环境(相当于一个符号查找表);
- 闭包和函数最大的区别在于,当捕捉闭包的时候,它的自由变量会在捕捉时被确定,这样即使脱离了捕捉时的上下文它也能正常运行; 在MDN中闭包的定义:
- 一个函数和其周围状态(词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包;
- 也就是说闭包可以让你在一个内层函数中访问到外层函数的作用域;
- 在JavaScript中,每当创建一个函数,闭包就会在函数创建的同时被创建出来
这些定义是不是看起来很“高大上”甚至看不懂表达的是什么?没有关系,接下来我们一起来看看究竟怎么解释闭包。
2.闭包是怎么出现的
首先我们必须知道Js中函数是一等公民,那么什么是一等公民呢?
一等公民意味着在Js中函数的使用是非常灵活的,它可以作为另一个函数的参数也可以作为另一个函数的返回值来使用所以在当函数作为另一个函数的返回值或作为另一个函数的参数,并且对外层的变量有所关联时就会闭包就出现了。总的来说闭包需要满足是三个条件
- 访问所在作用域;
- 函数嵌套;
- 在所在作用域外被调用;
总结:
我认为一个普通的函数function,如果它可以访问外层作用域的自由变量,那么这个函数就是一个闭包
从广义的角度来说:JavaScript(以下简称Js)中的函数都是闭包。如:
var name='foo; function foo(){}这个函数也是一个闭包,因为foo函数可以访问外层作用域的name变量从狭义的角度来说,Js中的函数如果访问了外层作用域的变量那么它才会构成一个闭包,如:
var name='foo; function foo(){console.log(name)}
3.闭包引起的内存泄漏
了解内存泄漏的小伙伴肯定都知道闭包可能会引起内存泄漏,而为什么闭包会产生这样的现象呢?我们一起来探索一下。 1. 首先我们先创建一个闭包的环境
function foo() {
var name='foo'
var age=18
function bar(){
console.log(name);
console.log(age);
}
return bar
}
var fn=foo()
fn()
2.我们从内存的角度来分析一下代码的执行
- ① 当函数刚开始被解析的时候(此时代码还没有执行,只是通过Js引擎如:V8引擎对代码进行了解析),会在内存中产生一个全局对象GlobalObject(以下简称GO)。根据解析的内容将fn,foo放入Go中,此时还没有对运行代码所以fn的值为undefined并且因为foo是一个函数会在堆内存中开辟新的空间在Go中存放的是foo的地址。
- ②解析完成后按代码顺序执行代码首先执行
var fn=foo(),在执行foo()时会先在栈中产生一个关于foo函数的FEC(函数执行上下文),并对foo函数预解析产生foo函数所需的AO对象(此时只是对foo函数的预解析并没有实际执行foo函数)。AO对象中存放name、age及bar函数的地址,到此时函数预解析完成下一步就是执行foo函数。
- ③执行foo函数将name的值修改为foo,age的值为18。对于bar函数对的定义则不需要关心,最终返回bar函数。到此foo函数执行完成,所以会将全局变量fn的值指向bar函数的地址0xb00并将foo函数产生的FEC出栈。
- ④此时代码继续执行,执行最后的
fn()同②③相同,执行fn之前会先将fn产生的FEC入栈,并对bar进行预解析产生相应的AO对象,因为bar函数中没有产生新的变量所以AO是一个空对象.
- ⑤此时执行bar函数中的
console.log(name);console.log(age);在执行过程中我们先在自身的AO对象中查找所需的变量,显而易见bar函数自身并不存在name和age变量。因此我们顺着作用域链向上查找在foo函数的AO对象中找到name和age的值并对其进行打印。此时整个fn()执行完成fn所对应的EFC出栈并且回收产生的AO对象,至此整个代码执行完成。
看到这里可能有小伙伴会疑惑为什么bar函数产生的AO对象就被 GC(垃圾回收机制) 回收而foo函数产生的AO对象就没有被回收呢?此时我们先来了解一下Js中的垃圾回收机制是什么样的(本文不过多阐述不了解的小伙伴们们可以详细去看看)。
Js中垃圾回收机制采用的多是标记清除法而标记清除法的原则是:
从根节点开始标记,然后顺着根节点的引用,将可达的变量也标记。直到有未访问的引用为止。清除那些没有被标记的变量。
我们可以从上图看到根节点就是Go,顺着根节点的引用可以找到fn与bar的引用关系,并且bar与foo产生的AO对象也存在着引用关系,所以此时GC并不会认为这个AO是需要被清理的垃圾因此它就被保存了下来。
3.消除闭包产生的内存泄漏
我们可以清除的看到AO对象是没有被释放的,如果我们后续还需要用到则只需要执行
fn()即可,如若不需要,那么它的存在就是一种浪费内存影响性能的体现。
那么我们如何消除闭包产生的内存泄漏呢?
我们通过上面的描述了解了内存泄漏产生的原因是fn函数与bar函数存在引用关系,我们只需要破坏掉这一层引用关系,执行
fn=null将fn指向的地址改为null。因为fn与bar函数失去了引用关系,所以bar函数变成不可达(从根元素无法追踪到bar函数)的了,所以在GC下一次轮询后就会判定bar函数是需要被释放的于是便释放bar函数占用的内存。
同理只需要执行
foo=null便可释放foo函数所产生的的内存空间
此时因闭包所产生的内存泄漏就被解决了。
在这篇文章中我们讲解了闭包的产生、闭包造成的内存泄漏及对于闭包产生的内存泄漏的解决方法,希望可以通过这篇文章对您的学习有一些帮助,如果喜欢的话还希望帮忙点点赞呀。