//这是昨天我测试的用例2:
var run = function () {
//一个占1MB内存的字符串
var str = new Array(1000000).join(''*'');
var doSth = function () {
if (str === 'something')
console.log("str was something");
};
doSth();
var log = function () {
console.log('interval');
};
setInterval(log, 100);
};
setInterval(run, 1000);
讨论闭包(Closure)
闭包是JavaScript中的一个基本概念。咋一看感觉没什么了不起:不就是在函数中定义了一个嵌套函数,并且嵌套函数能够访问外部变量,然后这个嵌套函数就起个名叫Closure嘛~
没错,在计算机科学文献中闭包的定义不过如此,但你不一定会用。举个例子:
在这个例子中外部函数返回了一个闭包inside,然后在全局作用域下调用了返回的这个函数,然后最终返回‘global’。
看上去很简单,不是么?
如果要真是这样的话那就没意思了。。。真实的情况是调用闭包后返回“local”!
为什么?正常情况下在全局声明了一个变量,然后在全局中调用一个函数:
理论上
一只变量
并没有在inside函数内部声明,因此他应该是个全局变量,这个函数应该返回‘global’才对啊~~不过这次注意inside函数是怎么来的:在一个外部函数中返回了这个inside闭包!
我想曾经学过类C语言的同学会对其函数有这样的印象:当函数执行完毕后,其内部定义的任何变量都被释放,并且不允许任何外部引用接触到内部的变量。因此大部分人对JavaScript的闭包存有误解:即使返回了一个内部定义的函数,函数不过是函数,像个模型一样等着任意变量去填充,所以内部定义的变量应该与inside内的变量无关并且误认为应该已经被释放了。
不过C语言不是一门函数式语言,也没有Closure一说。
所以事实是这样的:inside被返回后,由于闭包的特性,它内部会保持对其外部变量的引用,并且函数中定义的局部变量就不会被释放。这里有段来自权威指南中的底层解释:
那么闭包如何做到引用外部函数定义的变量呢?下面来说说JavaScript中的作用域链。
作用域链(Scope Chain)
作用域对象
在浏览器的全局作用域下,
window
为对应环境的全局变量,任何直接在该环境中声明或直接调用的变量都将成为window对象的属性。类比
window
对象,可以这么理解作用域:每个作用域都对应着一个‘对象’,称其为‘该作用域的全局对象’。因此,在函数中定义的变量可理解为该函数作用域的作用域对象的属性,也就是所说的变量属于这个局部作用域。
scope chain
当作用域形成一级一级的嵌套,便形成了作用域链。它是一个由每个作用域对应的作用域对象链接而成的一个对象链表。
在顶级作用域window,作用域链就包含window对象,函数作用域内,作用域链至少包含window对象和当前作用域对象,当嵌套时还有更多的函数作用域对象挂载到链上。
作用域链的特点就是:1.每执行一次函数,都会新建一个作用域对象并挂载到作用域链上,这个作用域对象保存了当前环境下的局部变量和函数参数。这是很好理解的,因为每次执行同一个函数,他们之间的作用域都互不干扰。2.当函数执行完毕,就将这个作用域对象从作用域链中删除(也就是局部变量被释放无法使用),但这成立与否基于一个前提,也就是前面所说的是否返回闭包的情况。
反观Closure
有了作用域对象的概念,就能进一步说解释包调用了。
以上是来自权威指南中的一段关于‘闭包如何保存外部变量’的描述。
词法作用域
但究其作用域对象不被释放的根本,还需要词法作用域这个概念,其规则是:函数定义时的作用域链到函数执行时依然有效!
JavaScript函数执行时用到了作用域链,而这个作用域链是在函数定义时确定的。由于嵌套函数定义在函数作用域内,其作用域链包含父函数的作用域对象,所以不管在何时何地执行闭包,这条作用域链都是有效的!
这回闭包的profile是不是就清晰了呢?然后让我们来看看闭包在实际应用中最常见的问题——内存泄漏。
JavaScript中的内存泄漏 [ Update at 2015.6.3 ]
就在昨天,我在浏览器中测试的一段代码引发了内存泄露,并且无法阻止,最后只好关掉重启浏览器。也正是这次的经历,让我对内存泄漏记忆颇深,并且决对不是空谈。
what's 内存泄漏?
所谓内存泄漏是指分配给应用的内存既 不能被回收 也 不能被重新分配利用 。
循环引用中的内存泄漏
举个循环引用的例子,例如:
垃圾回收算法
如果解释器采用了这种GC算法, 那么循环引用肯定会导致内存泄露, 因为目标对象的引用计数始终大于0. 不过这种算法已被现代浏览器所淘汰了, 大都经过改进, 结合了另一种GC算法:
正常来讲,这不会出现什么问题。但是当引用的对象之一关联着特占内存的大型数据(像超长字符串、超长哈希映射或DOM对象),问题就来了:
这是一个蹩脚的由链式引用引起的内存泄漏。
从浏览器和Node环境下的内存监测来看,每次GC似乎都没有彻底释放内存,显然内存泄漏了。我们来分析一下代码看看这是为什么:
首先,我定义一个显式引用org的闭包
unused
并且没有调用它. org在rep函数的公共词法作用域中.第二,sth和org一个道理, 会被unused隐式引用.
最后, unused能被
sth.fun
函数引用(虽然很明显这个函数没有使用它)。但是因为unused也在rep公共词法作用域内,fun还会保持对unused的引用。这样就形成了一个链式引用:org指向上一次的sth, unused引用org,而sth.func引用了unused。在当前时刻, 全局变量sth的属性fun作用域中隐式引用了unused, 也就间接引用了unused作用域的引用.
理论上, 这样就保存了一个引用链:
那这个问题该如何解决?对此,有两个解决方案:
1.由于unused函数引用了org,所以可以从这里入手,把unused这个函数中的引用去掉,这样就没有循环引用的闭包了,也就没有泄漏了。
2.将unused和sth引用的根源
org
做下处理:我测试了100s内的内存占用情况变化, 并做了可视化:
其中灰色代表原始代码, 粉色代表方案二, 淡蓝色代表方案一
这与我们之前的分析稍微有些出入. 严格来讲, 现代JavaScript引擎已经做到足够智能和优化(就像之前提到的两个GC算法), 如果沿着全局对象不能访问到目标对象, 那么他将被标记为不可用, 因此将被GC. 这就是为什么灰色的原始曲线也会有大幅波动出现.
还没读V8GC的代码, 暂时有这两个猜测 (接下来两段为猜想...)
既然可以被GC, 那么灰色曲线的持续增长又是什么原因? 这里我只能猜测引用链里有显示引用(unused对org)的原因导致每次GC时无法做出准确判断到底该不该回收, 只好等到内存使用量太大并且程序确实不会再用到他们时才GC一次.
或者是GC可以回收那些内存, 只是速率不及内存使用的快, 并且对于包含显示引用的引用关系回收效果甚微(就是不能一次回收干净, 要一点点的拆除ref) 脑补结束, 欢迎指明误区并参与讨论!!
而对于两种解决方案来说, 第一种能够"化显为隐"(unused中不存在对org的显示引用了). 第二种则更直接的断开了引用链(被显示引用的org变成null), 导致上一次产生的str及不可访问又失去了最后一个引用. 这就能让GC快速准确的回收内存.
可以看出, 两种方案的效果基本一样, 内存占用基本保持在一个小范围内波动, 只不过_赋值null_能比_去掉unused引用_更早的进行垃圾收集. 但未经处理的代码就不一样的了, 可以清除的看到灰色区域的内存占用量始终成上涨趋势, 虽然可以看到有进行内存收集的迹象, 但仍抵不住疯狂增长的大潮.
闭包中的链式引用
再来看下面这个明显的例子:
显而易见,
setInterval(run, 1000)
执行后run函数将会在每秒执行一次;log函数将会在一秒内连续执行10次。这里的
setInterval(log, 100)
保存了对log闭包的外部引用,因为str字符串在公共词法作用域内,所以log保持着对str的引用使str不会在run函数结束时被释放,并且在每次执行run时内存占用量都会增加。(完)