GC 算法?JS 的闭包?

407 阅读4分钟

一、JS 的内存管理

1. JS 的内存管理

  • JavaScript 会在定义变量时为我们分配内存
  • 内存分配的方式是一样的吗?
    • JS 对于基本数据类型内存的分配,会在执行时直接在栈空间进行分配
    • JS 对于复杂数据类型内存的分配,会在堆内存中开辟一块空间,并且将这块空间的地址保存在栈空间

2. JS 的垃圾回收

  • 垃圾回收的英文是 Garbage Collection,简称 GC
  • 对于那些不再使用的对象,我们都称之为垃圾,它需要被回收,以释放出更多的内存空间
  • JavaScript的运行环境 js引擎都会内存垃圾回收器

3. 常见的两个 GC 算法

GC怎么知道哪些对象是不再使用的呢?这里就要用到 GC 算法了

3.1 引用计数
  • 当一个对象有一个引用指向它的时候,那么这个对象的引用就 +1,当一个对象的引用为 0 时,这个对象就可以被回收掉

  • 但是这个算法有一个很大的弊端,就是会产生循环引用

    var obj1 = {friend: obj2}
    var obj2 = {friend: obj1}
    

image-20220326000211839.png


3.2 标记清除
  • 这个算法是设置一个根对象(root object),垃圾回收器会定期从这个根开始,找到所有从根开始有引用到的对象,对于那些没有引用到的对象,就认为是不可用的对象

image-20220326000504460.png

​ 图中从 A 开始找,找到 D 的时候结束,M,N不可达,被认为是不可用的对象

  • 这个算法可以很好的解决循环引用的问题

注:JS 引擎比较广泛采用的就是标记清除算法,当然类似于 V8 引擎为了进行更好的优化,在算法的实现细节上也会结合一些其他的算法

二、JS中的闭包

1. 什么是闭包?

JavaScript 中的一个函数,如果它访问了外层作用域的变量,那么这个函数是一个闭包。

MDN中的一个解释:一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)

function foo() {
    var name = "foo"
    var age = 18
    function bar() {
        console.log(name)// name访问了外层作用域的变量name
        console.log(age) // age访问了外层作用域的变量age
    }
    
    return bar // 返回一个函数
}

var fn = foo()
fn()

2. 闭包的访问过程

简单描述上面函数的执行过程:

1. GO: {foo:地址1, fn:undefined}
2. 执行代码:
   2.1 foo():只要执行函数,就会创建一个函数执行上下文
     (1)VO: {AO对象:{name: undefined;age:undefined;bar:地址2}}
        scopechain:[VO+parent scopes]
	 (2)开始执行代码 {name: "foo", age:18;} return bar地址2
	 (3)foo执行完毕
   2.2 fn: bar地址2
   2.3 fn(),即执行 bar地址2中的函数执行体,创建bar的函数执行上下文
   	   (1){AO:{},scopechain}
       (2)执行代码:
   	   console.log(name):在自己的AO中找不到,通过作用域链找到foo的AO,找到name:"foo",输出 "foo"
	   console.log(age)同理
	   (3)fn()执行完毕
3.执行完毕
地址1:foo函数对象: {parentScope: GO},{foo函数的执行体}
地址2:bar函数对象: {parentScope: foo的AO对象},{bar函数的执行体}

你可能会有疑惑:foo函数执行完毕之后,不是应该弹出栈吗,foo的AO对象不应该被释放了吗?怎么bar还能找到 name,找到 age 呢?

看下图:

image-20220326190828378.png

可以看到,当我们执行完 foo 函数的时候,返回的是 bar 函数(或者说是bar的地址),然后因为 fn = foo(),把这个地址赋值给了fn,因此会存在 fn 指向 bar函数对象,而bar的父作用域又指向了 foo 的AO对象,因此foo的AO是不会被释放的

3. 闭包的内存泄漏

为什么总说闭包是有内存泄漏的呢?什么是内存泄漏?

拿上面的例子说,如果后续我们不会再用 foo,bar 这些函数了,但是在全局作用域下 fn 变量对bar函数对象有引用,而bar的作用域中AO对foo的AO有引用,所以会造成这些内存都是无法被释放的。

这就是我们所说的内存泄漏,其实就是刚才的引用链中的对象无法释放

怎么解决这个问题呢?

很简单,设置 fn = null,就不再对 bar 函数对象有引用,那么从GO出发,bar是不可达的,那么对应的AO对象(foo) 同样也就不可达。

在下一次 GC 的的检测中,它们就会被销毁掉

还有一个问题,形成闭包之后,是不是所有的属性都不会被释放呢?

还是这个例子

function foo() {
  var name = "why"
  var age = 18

  function bar() {
    console.log(name)
    // console.log(age) 
  }

  return bar
}

var fn = foo()
fn()

如果age不使用了,会不会被销毁掉呢? 答案是会的,测试如下:

image-20220327110308371.png

image-20220327110407999.png

这是因为 V8 引擎做的优化,因为规范中闭包的属性应该是不会被销毁的