一、内存管理
在代码执行过程中都是需要给分配内存。不同的是,有些编程语言需要我们手动管理内存,有些编程语言可以自动帮我们管理内存:
- 手动管理内存:如C、C++
- 自动管理内存:如Java、JavaScript、Python
不管以什么方式来管理内存,内存管理会有以下生命周期:
- 申请并分配需要的内存
- 使用分配的内存去存放内容
- 不使用时对内存进行释放
1-1、内存分配
JavaScript会在定义变量时进行内存分配,不同数据类型分配方式不同:
- 基本数据类型:会在执行时直接在栈空间进行分配
- 复杂数据类型:会在堆内存中开辟一块空间,并将这块空间的内存地址值(指针)给变量
1-2、释放内存(JS的垃圾回收:Garbage Collection)
由于内存大小是有限的,当内存不再需要时,要对其进行释放。
对于JavaScript来说,其运行环境js引擎通过垃圾回收器(简称GC)去回收不需要的内存。
这就涉及到了GC算法,常见的GC算法有两种:
- 引用计数
- 标记清除
1-2-1、引用计数
当一个对象有一个引用指向它时,那么该对象的引用数 + 1。
当一个对象的引用数为0时,这个对象就可以被销毁了。
比如现在有以下三个对象:
var obj = { name: 'zs' }
var a = { name: 'lisi', friend: obj }
var b = { name: 'wangwu', friend: obj }
这段代码中,堆内存中会开辟三块空间去存储这三个对象,并将这三个空间的地址值分别赋值给了obj、a、b。
- 其中,a中的属性friend指向obj、b中的friend也指向obj,所以obj的引用数变为了3
- 当其引用计数变为0时才会被垃圾回收器所回收:若 a.friend = null,则其引用数会减1 但引用计数存在一个很大的弊端,若产生了循环引用,则会造成内存泄漏(内存无法得到释放):
比如如下代码:
var obj1 = { name: 'lisi', friend: obj2 }
var obj2 = { name: 'wangwu', friend: obj1 }
此时,obj1中的friend指向obj2而obj2中的friend又指向obj1:
1-2-2、标记清除
该算法是设置一个根对象(root object),垃圾回收器会定期从这个根对象开始去找所有引用到的对象,对于没有引用到的对象会认为是不可用的对象,会释放这些不可用对象的内存。而对于那些不可达的对象,它们的内存空间也会被释放掉。(不可达:即从根对象出发找不到的,这就很好解决了引用计数中循环引用的问题)
如图所示,从根对象A出发,H与I是不可达的(它俩之间是互相引用的关系),这样的话H与I会被JS引擎的垃圾回收器所回收
二、闭包
MDN对JavaScript的解释为:
一个函数和对其周围状态(词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包
- 闭包可以在一个内层函数中访问到外层函数的作用域
- 在JavaScript中,每当创建一个函数,闭包就会在函数创建的同时被创建出来
可以理解为:
- 一个普通的函数,如果它可以访问到外层作用域的自由变量,那么这个函数就是一个闭包
2-1、闭包的访问过程
function foo() {
var name = 'foo';
function bar() {
console.log(name);
}
return bar;
}
var fn = foo();
fn();
画图理解:
-
编译阶段:
- 创建GO对象及foo函数对象,GO中包含全局变量fn(此时为undefined)以及函数对象的地址值
- foo函数对象中父级作用域信息为“全局作用域”
-
执行阶段:
- 全局执行上下文入栈执行,执行var fn = foo()
- foo函数执行上下文及其对应的AO被创建,AO中包含变量name,此时为undefined。并创建bar函数对象,bar函数对象的父级作用域为foo
- foo函数执行上下文入栈执行,将其AO中的name变量赋值为foo,然后返回bar函数对象地址值
- foo函数执行完毕,其执行上下文出栈销毁
- fn变量便接收到了bar函数对象的地址值,此时GO中的fn变量的值变为bar函数对象的地址值
- 执行fn,fn函数也就是bar函数对应的执行上下文及AO被创建
- bar函数执行上下文入栈执行,输出name,其AO中不存在name,则去其上层作用域也就是foo函数作用域去寻找,找到了name。所以输出foo
- bar函数执行完毕,其执行上下文被销毁
- 代码执行过程详细讲解请见上一篇文章~
可见:
这里就形成了闭包,这里的闭包就由bar函数本身及其可访问的自由变量name组成
注意:bar函数之所以能访问到父级作用域中的name,只因为foo函数执行完后,foo函数的AO对象也就没有被销毁
- 从根对象GO出发,其中fn指向bar函数对象,而bar函数对象中的父级作用域指向foo函数的AO对象,所以foo函数的AO对象不会被销毁(根据标记清除算法可以得知:1-2-2)
2-2、为什么说闭包容易产生内存泄漏
以上这种情况,由于bar函数对象中父级作用域指向了foo函数的AO对象,导致foo函数的AO对象一直占着内存;而foo的AO对象的bar变量也一直引用着bar函数对象,所以bar函数对象也一直占用着内存。这样会导致内存泄漏。
解决方法:
- 若bar函数只调用一次,可以这样解决:
function foo() {
var name = 'foo';
return function() {
console.log(name);
}
}
var fn = foo();
fn();
/**....中间可能有其他代码 */
fn = null;
foo = null;
这样的话,从GO对象出发,fn没有了指向,foo也没有了指向:
- fn没有了指向,即切断了fn变量与bar函数对象之间的引用关系
- foo没有了指向,即切断了foo变量与foo函数对象之间的引用关系
- 这样的话,从根对象出发,foo函数对象、bar函数对象、foo函数的AO对象都变成了不可达的
虽然foo的AO和bar函数对象还是互相引用的关系,但是从GO根对象去出发,这些都是不可达的,所以后续都会被垃圾回收器所回收,这样就避免了内存泄漏的问题!