文章备份地址点这里javascript
闭包,是javascript的一大理解难点,网上关于闭包的文章也不少,可是不多有能让人看了就完全明白的文章。究其缘由,我想是由于闭包涉及了一连串的知识点。只有把这一连串的知识点都理解透彻,实现一个概念的闭环,才能够真正理解它。今天打算换个角度来理解闭包,从内存分配与回收的角度阐述,但愿能帮助你真正消化掉所看到的闭包知识,同时也但愿本文是你看的最后一篇关于闭包的文章。java
你们看本文中的配图时,请牢记箭头的指向。由于它是根对象window遍历内存垃圾所依赖的原则,可以从window开始,顺着箭头找到的都不是内存垃圾,不会被回收掉。只有那些找不到的对象才是内存垃圾,才会在适当的时机被gc回收。node
函数嵌套函数时,内层函数引用了外层函数做用域下的变量,而且内层函数被全局环境下的变量引用,就造成了闭包。数组
闭包实质上是函数做用域的副产物。浏览器
关于闭包咱们须要特别重视的一点是函数内部定义的全部函数共享同一个闭包对象。
什么意思呢?看以下代码:闭包
var a
function b() {
var c = new String('1')
var d = new String('2')
function e() {
console.log(c)
}
function f() {
console.log(d)
}
return f
}
a = b()复制代码
上面代码中f引用了变量d,同时f被外部变量a引用,因此造成闭包,致使变量d滞留在内存中。咱们思考一下,那么变量c呢?好像咱们并无用到c,应该不会滞留在内存中吧。而后事实是c也会滞留在内存中。如上代码造成的闭包包含两个成员,c和d。这种现象成为函数内闭包共享。函数
为何说须要特别重视这个特性呢?由于这个特性,若是咱们不仔细的话,很容易写出致使内存泄漏的代码。工具
关于闭包的概念性的东西,我就讲这么多了,可是若是真正理解好闭包,仍是须要搞明白几个知识点ui
这些内容你们能够谷歌百度之,大概理解一下。接下来我会讲如何从浏览器的视角来理解闭包,因此不作过多讲解。google
现代浏览器的垃圾回收过程比较复杂,详细过程你们能够自行google之。这里我只讲如何断定内存垃圾。大致上能够这么理解,从根对象开始寻找,只要能顺着引用找到的,都不能被回收。顺着引用找不到的对象被视为垃圾,在下一个垃圾回收节点被回收。寻找垃圾,能够理解为顺藤摸瓜的过程。
从最简单的代码入手,咱们看下全局变量定义。
var a = new String('小歌')复制代码
这样一段代码,在内存里表示以下
在全局环境下,定义了一个变量a,并给a赋值了一个字符串,箭头表示引用。
咱们再定义一个函数:
var a = new String('小歌')
function teach() {
var b = new String('小谷')
}复制代码
内存结构以下:
一切都很好理解,若是你细心的话,你会发现函数对象teach里有一个叫[[scopes]]的属性,这是什么东东?函数建立完为何会有这个属性。很高兴你能问到这一点,也是理解闭包很关键的一点。
请谨记:
函数一旦建立,javascript引擎会在函数对象上附加一个名叫做用域链的属性,这个属性指向一个数组对象,数组对象包含着函数的做用域以及父做用域,一直到全局做用域
因此上图能够简单理解为:teach函数是在全局环境下建立的,因此teach的做用域链只有一层,那就是全局做用域global
须要明确的是,浏览器下global指向window对象,nodejs环境global指向global对象
请再次谨记:
函数在执行的时候,会申请空间建立执行上下文,执行上下文会包含函数定义时的做用域链,其次包含函数内部定义的变量、参数等,当函数在当前做用域执行时,会首先查找当前做用域下的变量,若是找不到,就会向函数定义时的做用域链中查找,直到全局做用域,若是变量在全局做用域下也找不到,则会抛出错误。
咱们都知道,函数执行的时候,会建立一个执行上下文,其实就是在申请一块栈结构的内存空间,函数中的局部变量都在这块空间中分配,函数执行完毕,局部变量在下一个垃圾回收节点被回收。OK,咱们再次升级一下代码,看一下函数运行时内存的结构。
var a = new String('小歌')
function teach() {
var b = new String('小谷')
}
teach()复制代码
内存表示以下:
很明显,咱们能够看到,函数在执行过程当中仅仅作了一个局部变量的赋值,并未与全局环境下的变量发生关系,因此咱们从window对象沿着引用(图中的箭头)寻找的话,是找不到执行上下文中的变量b的。所以函数执行完后,变量b将被回收。
咱们再次升级一下代码:
var a = new String('小歌')
function teach() {
var b = new String('小谷')
var say = function() {
console.log(b)
}
a = say
}
teach()复制代码
内存表示以下:
注:灰色表示的是没法从根对象跟踪到的对象。
函数执行顺序:
函数执行完毕,正常状况下变量b应该被释放了。可是咱们发现,沿着window找下去,是可以找到b的,根据咱们前面讲的断定内存垃圾的原理得知,b不是内存垃圾,因此b不能被释放,这就是为何闭包会让函数内变量保存在内存中的缘由。
再次升级代码,咱们看下闭包共享的内存表示:
var a = new String('0')
function b() {
var c = new String('1')
var d = new String('2')
function e() {
console.log(c)
}
function f() {
console.log(d)
}
return f
}
a = b()复制代码
灰色表示的图形是内存垃圾,将会被垃圾回收器回收。
上图很容易得出,虽然函数f没有用到变量c,可是c被函数e引用,因此变量c存在于闭包closure中,从window对象开始寻找可以找到变量c,因此变量c也不能释放。
你也许会问了,这种特性是如何能致使内存泄漏的呢?好吧,思考以下一段代码,比较经典的meteor内存泄漏问题。
var t = null;
var replaceThing = function() {
var o = t
var unused = function() {
if (o)
console.log("hi")
}
t = {
longStr: new Array(1000000).join('*'),
someMethod: function() {
console.log(1)
}
}
}
setInterval(replaceThing, 1000)复制代码
这段代码是有内存泄漏的,在浏览器中执行这段代码,你会发现内存不断上升,虽然gc释放了一些内存,可是仍然有一些内存没法释放,并且是梯度上升的。以下图
这种曲线说明是有内存泄漏的,咱们能够经过开发者工具去分析哪些对象没有被回收掉。事实上我能够告诉你们,没有释放掉的内存其实就是咱们每次建立的大对象t。咱们经过画图的方式来看下:
上面这张图是假设replaceThing函数执行了三次,你会发现,每次咱们给变量t赋予一个大对象的时候,因为闭包共享的缘故,以前的大对象仍然可以从window对象跟踪到,因此这些大对象都不能被回收掉。其实真正对咱们有用的是最后一次为t赋予的大对象,那么以前的对象则形成了内存泄漏。
能够想象,假如咱们没有意识到这一点,任由程序一直运行下去,浏览器很快就会崩溃。
解决这个问题的方式也很简单,每次执行完代码,将变量o置为null便可,你们能够试试看哈~
文章到此结束,建议你们看一下本身曾经遇到的闭包例子,采用画图的方式,我想你会很容易的理解它。若是没有,欢迎和我私下沟通。