重学js(闭包和内存泄漏)

187 阅读4分钟

JS中函数是一等公民

  • 在JavaScript中,函数是非常重要的,并且是一等公民:
  • 函数和方法?
    • 函数 function :独立的function称之为函数
    • 方法 method:当函数属于某个对象时,称这个函数是该对象的方法
  • 那么就意味着函数的使用是非常灵活的;
  • 函数可以作为另外一个函数的参数,也可以作为另外一个函数的返回值来使用;
  • 自己编写高阶函数(把一个函数接收另外一个函数作为参数,或者该函数会返回另外一个函数作为返回值
  • 使用内置的高阶函数
  • filter((item,index,array)=>boolean)过滤 生成一个新数组
  • map((item)=>return item 操作 )映射 生成一个新数组
  • forEach((item)=> item 操作 )迭代
  • find/findIndex((item,index,array)=>return boolean )查找 结果的第一个元素
  • reduce((preValue 上一次的值,item这次的值 )=>({return item 操作 },initalValue 默认值 ) 累加 生成一个新数组

JS中闭包的定义

这里先来看一下闭包的定义,分成两个:在计算机科学中和在JavaScript中。

  • 在计算机科学中对闭包的定义(维基百科): 闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures);
  • 是在支持 头等函数 的编程语言中,实现词法绑定的一种技术; 闭包在实现上是一个结构体,它存储了一个函数和一个关联的环境(相当于一个符号查找表);-
  • 闭包跟函数最大的区别在于,当捕捉闭包的时候,它的 自由变量 会在补充时被确定,这样即使脱离了捕捉时的上下文,它也能照常运行;
  • 闭包的概念最早实现闭包的程序是 Scheme,那么我们就可以理解为什么JavaScript中有闭包 因为JavaScript中有大量的设计是来源于Scheme的;
  • 我们再来看一下MDN对JavaScript闭包的解释: 一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure);
  • 也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域;
  • 在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来;
  • 理解和总结:一个普通的函数function,如果它可以访问外层作用于的自由变量,那么这个函数就是一个闭包

闭包的访问过程

image.png

闭包的执行过程

那么函数继续执行呢?

  • 这个时候makeAdder函数执行完毕,正常情况下我们的AO对象会被释放;
  • 但是因为在0xb00的函数中有作用域引用指向了这个AO对象,所以它不会被释放掉;

image.png

闭包的内存泄漏

  • 在上面的案例中,该函数按理来说会被销毁掉,并且其引用着的父级作用域AO也应该被销毁掉
  • 但是目前因为在全局作用域下add10变量对0xb00的函数对象有引用,而0xb00的作用域AO(0x200)有引用,所以最终会造成这些内存都是无法被释放的

解决内存泄漏

  • 当将add10设置为null时,就不再对函数对象0xb00有引用,那么对应的AO对象0x200也就不可达了;
  • 在GC的下一次检测中,它们就会被销毁掉;

闭包内存泄漏测试

function createArray() {
      var arr = new Array(1024 * 1024).fill(1);
      //.fill代表填充 占据空间 int(4个字节)*1024*1024=4M
      function foo() {
        // 如果访问了外层作用域的变量,那么它是一个闭包
        console.log(arr.length)
      }
      return foo
    }

    // 保存引用地址到GO中的arrFns,让他们不被GC回收。
    var arrFns = [];

    for (var i = 0; i < 100; i++) {
      // 这里大概需要 100 * 100 = 10000 (10秒执行完毕)
      setTimeout(function () {
        // 保存引用,让其不被GC回收
        arrFns.push(createArray());
      }, i * 100)
    }
    //保存引用地址大概需要10秒,我们这里释放,在12秒以后开始释放一半的内存
    setTimeout(function () {
      for (var i = 0; i < 50; i++) {
        // 这里虽然是慢慢释放,但是在内存表现中,不会看到慢慢被释放的内存图
        // 因为GC内部的算法是定时回收的, 你也不知道他什么时候来回收已经被设置为null的变量
        setTimeout(function () {
          arrFns.pop()
        }, i * 100)
      }
    }, 12000)

image.png

AO不使用的属性

  • AO对象不会被销毁时,是否里面的所有属性都不会被释放?
    function foo() {
      var name = "why"
      var age = 18

      function bar() {
        debugger
        
        console.log(name)
      }

      return bar
    }

    var fn = foo()
    fn()
    // 我们不访问age age被释放了

结论: AO对象不会被销毁时,引用的自由变量不会被释放,没有被引用的自由变量会被释放,这里是V8引擎给我们做的一个优化。