美文网首页
js高级之内存管理与闭包

js高级之内存管理与闭包

作者: 一颗冰淇淋 | 来源:发表于2021-10-10 19:26 被阅读0次

    javacript中的内存管理

    javascript中不需要我们手动去分配内存,当我们创建变量的时候,会自动给我们分配内存。

    • 创建基本数据类型时,会在栈内存中开辟空间存放变量
    • 创建引用数据类型时,会在堆内存中开辟空间保存引用数据类型,并将堆内存中该数据的指针返回供变量引用
        var name = "alice"
        var user = {
            name: "kiki",
            age: 16
        }
    

    声明两个不同类型变量在内存中的表现形式如下


    javascript创建变量.png

    垃圾回收机制

    内存是有限的,当某些内存不需要使用的时候,我们需要对其释放,以腾出更多的内存空间,在javascript中有两种垃圾回收算法。

    1. 引用计数

    当对象有引用指向它的时候,计数增加1,消除指向就减少1,当计数为0时,对象会自动被垃圾回收器及销毁

    这样的回收机制可能存在问题,当两个对象循环引用时,这两个对象都不会被销毁,可能存在内存泄漏的情况


    引用计数.png

    2. 标记清除

    设置一个根对象,垃圾回收器会定期从这个根开始,找所有从根开始有引用到的对象,销毁没有引用到的对象

    这样一种算法可以比较有效的解决循环引用的问题,下图中MN从根节点中无法找到有引用的对象,所以会被垃圾回收器销毁


    标记清除.png

    函数的多种用途

    在javascript中,函数是非常重要且应用广泛的,它最常用有以下几种

    1、作为参数传递
    函数可以直接作为另一个函数的参数,并且直接调用执行,以下定义了多种计算数字的方法,加减乘,进行不同的运算不需要每次调用不同的函数,只需要改变传参即可

    function calcNum(num1, num2, fn) {
      console.log(fn(num1, num2))
    }
    function add(num1, num2) {
      return num1 + num2
    }
    function minus(num1, num2) {
      return num1 - num2
    }
    function mul(num1, num2) {
      return num1 * num2
    }
    calcNum(10, 20, add)  // 30
    

    2、作为返回值
    函数也可以返回一个函数,以下函数叫做高阶函数,也成为函数柯里化,可以多次接收返回并进行统一的处理

    function makeAdder(count) {
      function add(num) {
        return count + num
      }
      return add
    }
    var add5 = makeAdder(5)
    console.log(add5(6))
    console.log(add5(10))
    
    var add10 = makeAdder(10)
    var add100 = makeAdder(100)
    

    3、作为回调函数
    在数组中有很多方法都需要我们自定义回调函数来处理数据

    var nums = [10, 50, 20, 100, 40]
    var newNums = nums.map(function(item){
        return item * 10
    }) 
    

    闭包

    如果一个函数,可以访问到外层的自由变量,那么它就是闭包

    如以下代码所示,bar函数可以访问到父级作用域的变量name和age

    function foo(){
        var name = "foo"
        var age = 18
        function bar(){
            console.log(name)
            console.log(age)
        }
        return bar
    }
    var fn = foo()
    fn()
    

    以上代码执行结果为

    foo
    18
    

    按照代码的执行顺序来说,foo函数被执行完成,它的函数上下文已经从栈中弹出,而foo函数中的变量为什么还能被保存下来?

    因为foo函数执行上下文创建的时候,同时创建AO对象,AO对象仍然被bar函数的parentScope所指向,所以不会被垃圾回收器销毁

    以上代码在内存中的执行过程如下

    1. Javascript --> AST

      • 在内存中开辟空间0x100保存函数foo,其中保存父级作用域(parentScope)指向GO
      • 内存中存在GO(Global Object)对象,其中包括了内置的模块如 String、Number等,同时将定义的全局变量保存至GO中,这里将fn添加到GO中,值为undefined,函数foo添加到GO中,值为0x100
    2. Ignition处理AST

      • V8引擎执行代码时,存在调用栈 ECStack,创建全局执行上下文,VO指向GO
      • 创建函数foo的函数执行上下文,VO(variable object)指向foo的AO(
        active object)
        ,执行函数体内代码
      • 创建foo的AO对象,将name和age添加到AO中,值为undefined,
      • 执行代码前,给foo内的函数bar开辟内存空间0x200,bar函数的父级作用域指向AO
      • 将foo添加到AO对象中,值为0x200
    3. 执行代码

      • 函数foo的返回值bar函数赋值给fn,所以fn的值为bar函数的内存地址 0x200
      • 执行foo函数,将foo的AO对象中的name赋值为foo,age赋值为18
      • 执行fn函数前,bar函数创建函数执行上下文,VO指向bar的AO
      • 创建bar的AO,bar函数内没有定义变量,所以AO为空
      • 执行fn函数,输入name和age
    4. 执行完成

      • foo函数被执行完成,foo函数的执行上下文弹出调用栈
      • bar函数被执行完成,bar函数的执行上下文弹出调用栈
      • bar的AO对象是函数执行上下文存在时创建,此时也没有被其它地方引用,所以会被垃圾回收器销毁
      • bar函数赋值给了全局变量,不会被销毁,并且bar的父级作用域指向foo的AO对象,因此foo的AO对象也不会被销毁,所以在bar函数中能访问到foo中的变量

    图示如下


    闭包保存父级作用域的变量.png

    AO优化

    以上foo的AO对象有被引用,所以没有销毁,如果此时AO对象只是部分变量被引用,而其它变量没有用到呢,那没有用到的变量会被销毁吗?比如以下foo函数的变量age

    function foo(){
      var name = "foo"
      var age = 18
      return function(){
        console.log(name)
      }
    }
    var fn = foo()
    fn()
    

    按照ECMAScript规范是不会的,因为整个AO对象都被保存在内存中了,但是JS引擎可能会做一些优化,比如说Chome浏览器使用的V8引擎

    在以上闭包中增加debugger进行调试

    function foo(){
      var name = "foo"
      var age = 18
      return function(){
        debugger
        console.log(name)
      }
    }
    var fn = foo()
    fn()
    

    可以用两种方法测试到foo的变量age没有被保存下来

    1.在Sources中查看Closure保存的变量
    代码执行到debugger处,可以查看到闭包此时的作用域,父级作用域foo中只保存了变量name

    source中查看闭包.png

    2.在Console中输出变量
    当代码执行到debugger处,此时Console就是在闭包的执行环境中,可以直接打印变量,name可以直接被打印出来,而打印age则直接保存未定义

    console中查看.png

    内存泄漏

    如上述例子中被保存到全局的闭包,因为有互相的引用,不会被销毁,如果后续不再使用,就可能出现内存泄漏的情况。
    用以下代码测试一下

    function createFnArray() {
      var arr = new Array(1024 * 1024).fill(1)
      return function () {
        console.log(arr.length)
      }
    }
    
    var arrayFns = []
    for (var i = 0; i < 100; i++) {
      setTimeout(() => {
        arrayFns.push(createFnArray())
      }, i * 100)
    }
    
    setTimeout(() => {
      for (var i = 0; i < 50; i++) {
        setTimeout(() => {
          arrayFns.pop()
        }, 100 * i)
      }
    }, 10000)
    

    以上代码每隔0.1s创建一个内存容量为1024*1024的数组(约4M)保存到全局变量中,共计100个,再隔10s后将每隔0.1s从数组底部弹出一个元素,共计50个。

    这样操作在内存中的表现应为前10s陆续增加内存的使用,第10s时,内存占用约为400M,等到15s后,内存占用减少一半,因为垃圾回收器不会马上回收或销毁垃圾,所以可能会有一定的时间延缓

    内存的占用.png

    释放内存

    内存的大量占用会造成内存泄漏,当不需要使用的时候,要及时的释放,只需要将变量指向null,即可释放内存

    var fn = foo()
    // 无需使用时
    fn = null
    

    以上就是关于内存和闭包的理解,关于js高级,还有很多需要开发者掌握的地方,可以看看我写的其他博文,持续更新中~

    相关文章

      网友评论

          本文标题:js高级之内存管理与闭包

          本文链接:https://www.haomeiwen.com/subject/gtdholtx.html