02_JS内存管理和闭包
一、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}
3.2 标记清除
- 这个算法是设置一个根对象(root object),垃圾回收器会定期从这个根开始,找到所有从根开始有引用到的对象,对于那些没有引用到的对象,就认为是不可用的对象
图中从 A 开始找,找到 D 的时候结束,M,N不可达,被认为是不可用的对象
- 这个算法可以很好的解决循环引用的问题
注:JS 引擎比较广泛采用的就是标记清除算法,当然类似于 V8 引擎为了进行更好的优化,在算法的实现细节上也会结合一些其他的算法
二、JS中的闭包
1. 什么是闭包?
JavaScript 中的一个函数,如果它访问了外层作用域的变量,那么这个函数是一个闭包。
MDN中的一个解释:一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)
function foo() { |
2. 闭包的访问过程
简单描述上面函数的执行过程:
1. GO: {foo:地址1, fn:undefined} |
你可能会有疑惑:foo函数执行完毕之后,不是应该弹出栈吗,foo的AO对象不应该被释放了吗?怎么bar还能找到 name,找到 age 呢?
看下图:
可以看到,当我们执行完 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() { |
如果age不使用了,会不会被销毁掉呢?
答案是会的,测试如下:
这是因为 V8 引擎做的优化,因为规范中闭包的属性应该是不会被销毁的