JavaScript内存管理和闭包

JavaScript内存管理和闭包

  • JavaScript内存管理
  • 立即回收机制算法
  • 闭包的概念理解
  • 闭包的形成过程
  • 闭包的内存泄露

1.认识内存管理

1.1

不管什么编程语法,都会占用内存,比如变量,对象。某些编程语言需要手动管理内存(手动声明某个变量占用多大内存,手动释放内存),有的编程语言也可以自动管理内存。

不管什么方式管理内存,内存管理都会有如下生命周期:

  1. 分配申请内存
  2. 使用分配的内存
  3. 不需要使用的时候,对其进行释放

JavaScript是自动管理内存的。

内存我们分为两种:

  • 栈内存
  • 堆内存

在JavaScript中,原始数据一般放在栈内存中;复杂数据类型(引用数据类型)存放在堆内存中,栈内存的变量名放的是该复杂类型在堆内存里面的引用。

1.2 JavaScript的垃圾回收

什么是垃圾回收:

因为内存是有限的的,当一些内存不需要的时候,就需要对其进行释放,以便腾出更多内存。

手动管理内存比较麻烦,并且容易产生内存泄露。

垃圾回收的英文:Garbage Collection,简称GC。

垃圾就是在运行环境中不会被使用的变量、对象等等。

JavaScript的运行环境js引擎负责垃圾回收。

如何知道某些东西不再使用呢?这就需要GC算法了。

1.3常见的GC算法

  1. 引用计数算法(JavaScript没有用这个算法)

我们创建一个对象之后,它会放在堆内存里面,里面会有一个属性retainCount,在栈内存有一个变量存放了这个对象的内存地址,那么retainCount就会加一,反之则减一,如果当这个retainCount变为0的时候,这个对象就会被垃圾回收机制回收释放掉。

  • 这个算法有一个弊端,如果存在相互引用,如果不手动处理,那么retainCount永远不可能为0,那么永远不会被回收。
  1. 标记清除法

标记清除的核心思想:可达性

这个算法会设置一个根对象(root object ),垃圾回收器会定期从这个根对象开始查找能引用到的对象,如果存在没有引用到的对象,它就会认为这是一个不可用对象,这个不可达的对象就会被清除。

这个算法可以有效的解决相互引用的问题。

JavaScript中主要采用标记清除法,V8引擎还用了一些其他的算法。

补充:

  • 标记整理:与标记清理有点区别,他会将保留下来的一些对象放到连续的存储空间,整合空闲空间。并且可以减少内存碎片,提高内存利用率。
  • 分代收集:对象被分为新的与旧的,长期存在的对象,会变得老旧,被检查的次数就会减少。
  • 增量收集:可能对象会很多,全部遍历一遍的话,可能比较耗时,出现延迟,所以会将垃圾收集工作分为几部分来处理,每一部分逐一处理。
  • 闲时收集:在CPU空闲的时候运行,以便减少对代码执行的影响。

2. 闭包的概念理解

2.1 JavaScript的函数式编程

JavaScript是支持函数式编程的,在JavaScript中函数式非常重要的。

2.2 闭包的定义

  • 计算机科学中的闭包
    • 这是在支持头等函数的编程语言中,实现词法绑定的一种技术;它是一种结构,它存储了一个函数和一个关联的环境,在捕获闭包的时候,它的自由变量会在捕获时确定,即使脱离了捕获时的上下文,它也可以正常运行。
  • MDN对JavaScript闭包的解释:一个内层函数访问到其外层函数的作用域,在JavaScript中,每当创建一个函数,闭包就会在函数创建的同时创建出来。

在函数体内部可以访问到外部的变量,就是因为js内部用闭包为我们做了一些事情。

  • 总结:

    • 一个普通的函数,如果它可以访问外层作用域的自由变量,那么这个函数和周围环境就是一个闭包
    • 广义:JavaScript中的函数都是闭包
    1
    2
    3
    function foo(){

    } //这就是一个闭包
    • 狭义:JavaScript中一个函数,如果访问了外层作用域的变量,那么它就是一个闭包。
    1
    2
    3
    4
    5
    var name = "asd";

    function foo(){
    console,log(name); // 引用外部的变量
    } // 这就是一个狭义上的闭包

2.3 闭包的形成访问过程

1
2
3
4
5
6
7
8
9
function adderCreate(count){
function adder(num){
console.log(count+num);
}
return adder;
}

var adder5 = adderCreate(5);
adder5(4); // 9

上面的代码形成了闭包。

  1. 执行所有代码之前,创建一个全局对象(GO),里面包含adderCreate函数,以及adder5,此时adder5为undefined。
  2. 全局作用域中的adderCreate函数,也有自己的一个作用域,里面有adder函数的地址,并且还有一个参数count,此时参数为undefined。并且这个adderCreate还有一个作用域链,里面只有一个值,为GO(全局对象)。
  3. 接着就是adder函数,它有自己的作用域(AO),在这个作用域里面有num,此时为undefined,也有自己的作用域链:(0:adderCreate,1:GO)
  4. 执行创建函数代码,此时返回一个函数,返回的是adder函数的地址,所以此时GO里面adder5的值就变为返回的这个地址。
  5. 此时GO里面的adder5就指向adderCreate里面的adder函数,此时在这个函数里面可以访问到adderCreate的变量count(因为作用域链可以向上查找),这就形成了闭包。

并且上面创建的每一个函数,都可以从GO找到,所以都不会被垃圾回收机制回收,所以可能会造成内存泄露。

2.4 闭包的内存泄露

对于闭包不再使用的函数,垃圾回收机制不会自动回收,会一直存在,就会造成内存泄露。

这个时候就需我们手动进行内存释放。

比如adder5 = null;