前端学习笔记——关于闭包与内存泄漏

这个前端学习笔记是学习gitchat上的一个课程,这个课程的质量非常好,价格也不贵,非常时候前端入门的小伙伴们进阶。
在这里插入图片描述
笔记不会涉及很多,主要是提取一些知识点,详细的大家最好去过一遍教程,相信你一定会有很大的收获

作用域

就近原则,通过作用域链,找到最近的一个变量

  1. ES6的块级作用域

    使用letconst命名的变量,不会声明提升。

    在上述2个声明的变量前使用变量会报错,并且同一个作用域下只允许一个let声明同一个变量。

  2. 函数的参数也会出现死区TDZ,所以需要注意引用的顺序。

执行上下文和调用栈

  1. 执行上下文就是当前代码的执行环境/作用域

代码执行会经历2个阶段:

  1. 代码预编译(VO)

    降JS代码编译成可执行的代码。javascript是解释型语言,编译一行执行一行。执行前,javascript引擎会提前做些准备工作。确认语法无误的时候,javascript代码在预编译的阶段对变量的内存空间进行分配,在这过程中会经历:

    1. 变量声明
    2. 变量声明提升,值为undefined
    3. 非表达式函数声明提升
  2. 代码执行阶段(AO)

    此时作用域链已经确定,由当前作用域和外层所决定,保证了变量的有序访问。

调用栈

函数一个接着一个调用,形成的类似栈访问。符合先进后出的形式。

函数执行完之后,退出栈之后,函数的内部变量就会被垃圾回收器回收,这也是函数外部无法访问函数内部的原因。

正常的函数会经历3个阶段:

  1. 创建阶段
  2. 执行阶段
  3. 执行完毕,回收阶段

闭包

闭包就是为了解决无法访问函数内部的这个问题,函数执行完毕,空间不会马上销毁。

内层函数引用了外层函数作用域下的变量,并且内层函数在全局环境下可访问,就形成了闭包。

一个简单的闭包:

function numGenerator() {
    let num = 1
    num++
    return () => {
        console.log(num)
    } 
}

var getNum = numGenerator()// 函数执行之后,num不会马上消失
getNum()// 可以通过返回的函数进行访问

我们如果返回一个函数,而这个函数访问了上一个函数作用域下的变量,这样就能够让函数上下文在函数执行之后不会马上销毁。这也是闭包的基本原理。

内存管理

  1. 分配内存:声明的时候,会划分内存保存数据
  2. 使用内存:所有使用到变量的地方
  3. 销毁内存:手动等于null,或者执行完毕之后走出上下文环境等

堆内存和栈内存

javascript有2种数据类型:基本和引用

基本类型都保存在栈内存中,而引用类型保存在堆内存中,注意引用内存也会使用到栈内存,引用类型栈内存保存的是堆内存的地址。这里用一张图表示:

在这里插入图片描述
每一个语言都会有属于自己的垃圾回收器,在不用到的内存会进行释放,但是垃圾回收器也是不完美的,也会存在判断错误的情况,无法将垃圾内存进行回收。这样就会出现内存泄漏:指内存空间明明已经不再被使用,但由于某种原因并没有被释放的现象。

内存泄漏

这个是每一个程序员都需要关心的问题。我们来看看前端有那些常见的内存泄漏情形。

  1. 例子1:

    var element = document.querySelectorAll("li")
    
    

// 移除 element 节点
function remove() {
document.querySelector(‘body’).removeChild(element1[0])
}
```

我们在操作DOM元素的时候,如果响应移除掉一个元素节点,一般使用removeChild即可,但是上述情况中,element还保存着对已经移除的元素引用,虽然视觉上这个节点已经移除了,但是在dom对象上,还是会保存这个节点的信息。所以,我们需要手动的移除element这个节点。

在这里插入图片描述

  1. 例子2

    var element = document.getElementById('element')
    element.innerHTML = '<button id="button">点击</button>'
    
    var button = document.getElementById('button')
    button.addEventListener('click', function() {
        // ...
    })
    
    element.innerHTML = ''
    

    这里我们动态添加一个节点,但是我们给这个按钮添加了一个事件之后,想要清楚element节点的内容,虽然视觉上已经移除,但是在内存中,button还保存着button节点的信息和其事件处理句柄还在,垃圾回收器无法回收,需要手动清楚这些引用。

  2. 例子3

    定时器,如果不需要了就及时停止。

    function foo() {
      var name  = 'lucas'
      window.setInterval(function() {
        console.log(name)
      }, 1000)
    }
    
    foo()
    

    由于计时器的一直存在,name无法释放内存。

    如果业务不需要,就自己手动停止计时器。clearInterval

  3. 意外的全局变量

    function foo(arg) {
        bar = "this is a hidden global variable";
    }
    // 或者不恰当使用this
    function foo() {
        this.variable = "potential accidental global";
    }
    

    函数执行后,并不会消除bar的内存

垃圾回收机制

当然,除了开发者主动保证以外,大部分的场景浏览器都会依靠:

  • 引用计数

    这是一个算法,能够追踪没有被引用的对象,进行回收。通过例子来理解这个引用的意思。

    var obj1 = {
        property1: {
             subproperty1: 20
         }
    };
    
    

    obj1引用了一个对象property1,property1也引用了一个对象。由于obj1引用了一个对象,所以不会被回收。

    我们接下来继续进行操作:

    var obj2 = obj1;
    
    obj1 = "some random text"
    
    

    这时,obj2引用这obj1所引用的同一个对象,后面obj1不在引用对象,这时只存在一个obj2引用着这个对象。垃圾回收器也不会回收。

    我们继续操作:

    var obj2_pro = obj2.property1
    

    obj2_pro引用这obj2属性。这个时候,对象就有2个引用,一个是obj2,一个是obj2_pro

    我们消除obj2的引用

    obj2 = "some random text"
    

    但是还存在obj2_pro的引用,垃圾回收器无法回收。我们继续操作,消除obj2_pro的引用。

    obj2_pro = null
    

    这个时候,最初的对象没有任何引用了,这时引用垃圾回收器就可以进行回收了。

    假如,2个对象互相引用,那么这个就永远无法回收了。这就会导致内存泄漏。

  • 标记清除

    为了解决引用计数的弊端,这里还存在一种回收机制。

    我们从跟节点进行访问,把所有访问到的进行标记,所有可以遍历都遍历之后,存在没有被标记的对象,就可以进行回收。

    自 2012 年以来,JavaScript 引擎已经使用此算法来代替引用计数垃圾回收。

参考文章
通过垃圾回收机制理解 JavaScript 内存管理
四种常见的内存泄漏

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章