浏览器是怎么看闭包的。

本文从内存分配与回收的角度深入解析JavaScript闭包的原理,解释闭包如何让函数内的变量保持在内存中,以及闭包共享特性可能导致的内存泄漏问题。

文章备份地址点这里

闭包,是javascript的一大理解难点,网上关于闭包的文章也很多,但是很少有能让人看了就彻底明白的文章。究其原因,我想是因为闭包涉及了一连串的知识点。只有把这一连串的知识点都理解透彻,实现一个概念的闭环,才可以真正理解它。今天打算换个角度来理解闭包,从内存分配与回收的角度阐述,希望能帮助你真正消化掉所看到的闭包知识,同时也希望本文是你看的最后一篇关于闭包的文章。

大家看本文中的配图时,请牢记箭头的指向。因为它是根对象window遍历内存垃圾所依赖的原则,能够从window开始,顺着箭头找到的都不是内存垃圾,不会被回收掉。只有那些找不到的对象才是内存垃圾,才会在适当的时机被gc回收。

闭包简介

函数嵌套函数时,内层函数引用了外层函数作用域下的变量,并且内层函数被全局环境下的变量引用,就形成了闭包。

闭包实质上是函数作用域的副产物。

关于闭包我们需要特别重视的一点是函数内部定义的所有函数共享同一个闭包对象
什么意思呢?看如下代码:

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。这种现象成为函数内闭包共享。

为什么说需要特别重视这个特性呢?因为这个特性,如果我们不仔细的话,很容易写出导致内存泄漏的代码。

关于闭包的概念性的东西,我就讲这么多了,但是如果真正理解好闭包,还是需要搞明白几个知识点

  • 函数作用域链
  • 执行上下文
  • 变量对象、活动对象

这些内容大家可以谷歌百度之,大概理解一下。接下来我会讲如何从浏览器的视角来理解闭包,所以不做过多讲解。

如何判别内存垃圾

现代浏览器的垃圾回收过程比较复杂,详细过程大家可以自行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()

内存表示如下:

注:灰色表示的是无法从根对象跟踪到的对象。

函数执行顺序:

  1. 函数teach开始执行前,申请栈空间,上图蓝色方块。
  2. 创建上下文scope(类栈结构),并将teach函数定义时的[[scopes]]压入到scope中。
  3. 初始化变量b(变量提升),创建函数say,初始化say的scopes属性,首先将函数teach的scopes压入函数say的[[scopes]] 中。由于say引用了变量b,形成闭包closure。所以我们还要将closure对象压入函数say的[[scopes]]。
  4. 创建变量对象local,指向局部变量b和say,并将local压入步骤2的scope中。
  5. 函数开始执行
    1. 给变量b赋值字符串对象'小谷'。
    2. 将全局变量a指向函数say。

函数执行完毕,正常情况下变量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即可,大家可以试试看哈~

结语

文章到此结束,建议大家看一下自己曾经遇到的闭包例子,采用画图的方式,我想你会很容易的理解它。如果没有,欢迎和我私下沟通。


更多专业前端知识,请上 【猿2048】www.mk2048.com
<think>我们正在讨论闭包引起的内存泄漏问题是否仅存在于IE浏览器中。根据提供的引用内容,我们可以得出以下结论: 1. 引用[2]明确指出:在IE9之前的版本中,由于对JScript对象和COM对象使用了不同的垃圾回收机制,闭包可能导致内存泄漏。具体来说,当闭包中引用了DOM元素(如通过事件处理函数),而该闭包又被DOM元素引用(如赋值给onclick事件),就会形成循环引用,导致在IE9之前无法正常回收这些对象。 2. 引用[3]也提到,在IE浏览器中,DOM对象与JS对象相互引用会导致内存泄漏,并提供了解决方案(如手动解除引用)。 3. 引用[5]给出了一个在IE下闭包引起内存泄漏的案例,并提供了解决方案(将DOM元素引用置为null)。 4. 引用[4]展示了一个在非IE环境下(使用V8引擎)的闭包内存泄漏案例,说明即使是非IE浏览器,如果闭包使用不当(例如持有大对象的引用且没有及时释放),也会导致内存泄漏。但是,这个泄漏与IE的泄漏机制不同,它是由于闭包保留了对外部变量的引用,导致这些变量无法被垃圾回收,直到闭包本身被释放。 因此,我们可以总结: - **IE9之前的浏览器**:闭包确实会导致特定的内存泄漏问题,主要原因是IE的垃圾回收机制(对COM对象和JS对象分别管理)无法处理循环引用。这种泄漏是IE特有的问题。 - **现代浏览器(包括IE9及之后版本)**:使用标记清除(mark-and-sweep)等垃圾回收算法,可以处理循环引用,因此不会因为循环引用而导致内存泄漏。但是,如果闭包长期持有大对象的引用(比如在闭包中引用了一个大数组,而该闭包被长期使用),那么这些大对象将无法被回收,从而造成内存泄漏。这种泄漏与浏览器无关,而是由于编程不当导致的。 所以,闭包引起的内存泄漏问题并非仅出现在IE浏览器中,但在IE9之前的版本中,由于垃圾回收机制的缺陷,闭包更容易导致内存泄漏(而且是特定类型的泄漏)。在现代浏览器中,闭包引起的内存泄漏通常是由于程序员没有正确管理资源(比如没有及时释放不再需要的闭包)造成的。 引用: [^2]: 在IE9之前,闭包可能导致内存泄漏,因为IE对JScript对象和COM对象使用了不同的垃圾回收机制。 [^3]: IE浏览器中,DOM对象与JS对象相互引用会导致内存泄漏。 [^5]: 在IE下闭包引起的内存泄漏案例及解决方案。 [^4]: 非IE环境下(V8引擎)闭包也可能导致内存泄漏,但原因不同(闭包持有大对象引用且未释放)。 因此,回答用户问题:闭包引起的内存泄漏问题并非仅存在于IE浏览器,但在旧版IE中由于垃圾回收机制的问题,更容易出现特定类型的泄漏。现代浏览器中,闭包使用不当同样会导致内存泄漏。 ### 相关问题: 1. 如何避免闭包引起的内存泄漏? 2. 现代浏览器中如何处理循环引用? 3. 在JavaScript中,如何检测内存泄漏?</think>### 闭包引起的内存泄漏问题是否仅存在于IE浏览器闭包引起的内存泄漏问题**并非仅存在于IE浏览器**,但在IE浏览器(尤其是IE9之前版本)中表现得尤为严重且机制特殊。以下是关键分析: #### 1. **IE浏览器特有的泄漏机制(主要问题)** 在 **IE9之前版本** 中,闭包引起的泄漏是**由浏览器引擎缺陷导致**的: - **根本原因**:IE对 **JScript对象(JS对象)** 和 **COM对象(DOM元素)** 使用不同的垃圾回收机制,两者相互引用时无法自动回收[^2][^3]。 - **典型场景**: ```javascript function assignHandler() { let element = document.getElementById('someElement'); element.onclick = () => console.log(element.id); // 闭包引用了DOM对象,DOM对象又引用了闭包 → 循环引用 } ``` - **后果**:即使页面卸载,这些对象也不会被释放,导致内存持续增长[^1][^3]。 #### 2. **现代浏览器的泄漏情况** 在 **Chrome/Firefox/Safari/Edge 等现代浏览器** 中: - ✅ **循环引用可被回收**:使用**标记清除算法**(Mark-and-Sweep),能正确处理JS对象与DOM对象的循环引用[^4]。 - ⚠️ **仍可能人为泄漏**:若闭包长期持有**不必要的大对象引用**,仍会导致内存无法释放: ```javascript function createLeak() { const bigData = new Array(1000000).fill("data"); // 10MB数据 return () => console.log(bigData[0]); // 闭包持有大对象引用 } const leakyFunc = createLeak(); // 即使不再使用,bigData仍驻留内存 ``` #### 3. **解决方案对比** | 场景 | IE9之前方案 | 现代浏览器方案 | |---------------------|-------------------------------------|----------------------------------| | **循环引用泄漏** | 手动解除引用:`element = null`[^3][^5] | 无需特殊处理(自动回收) | | **大对象长期持有** | 同左 | 主动释放引用:`leakyFunc = null` | #### 4. **关键结论** - **IE浏览器**:闭包泄漏是**引擎级缺陷**,需开发者主动处理[^2][^3][^5]。 - **现代浏览器**:闭包泄漏通常是**代码设计问题**(如意外保留大对象引用),而非浏览器缺陷[^4]。 - **通用建议**: 1. 避免在闭包中**长期持有DOM对象**(尤其事件监听) 2. 及时解除**无用的大对象引用** 3. 使用`WeakMap`/`WeakSet`管理临时引用 > 示例:安全解除IE泄漏的代码[^5] > ```javascript > function safeHandler() { > let ele = document.getElementById("btn"); > const id = ele.id; // 复制值而非引用DOM > ele.onclick = () => console.log(id); > ele = null; // 主动解除DOM引用 > } > ``` ### 总结 闭包引起的内存泄漏在**所有浏览器中都可能发生**,但IE9之前的版本因垃圾回收机制缺陷导致问题更严重且必须手动修复。现代浏览器虽能自动处理循环引用,仍需开发者注意闭包中的对象引用管理[^2][^3][^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值