js堆栈溢出和内存泄漏
概念:
堆栈溢出: 程序内部函数的调用以及返回会不停的执行进栈和出栈的操作,栈空间是有限的,一旦调用即进栈过多就会导致栈满
内存泄漏: 申请的内存执行完之后没有及时的清理和销毁,占用空闲内存,既不能使用也不能回收
# 一、堆栈溢出
JS 中的数据存储分为栈和堆,栈遵循先进后出的原则,所以程序从栈底开始计算,程序内部函数的调用以及返回会不停的执行进栈和出栈的操作,一旦调用即进栈过多就会导致栈满。
这种情况常见于递归调用
# 1.什么是函数调用
var a = 2
function add() {
var b = 10
return a + b
}
add()
2
3
4
5
6
我用这段简单的代码来解释下函数调用的过程。
在执行到函数add()之前,JavaScript引擎会为上面这段代码创建全局执行上下文,包含了声明的函数和变量,你可以参考下图:
# 2.函数执行过程
生成可执行代码之后,JS引擎开始顺序执行代码,执行到add这里时,JS引擎判断出这里是函数调用,然后执行下面操作:
- 从全局上下文中,取出add函数代码
- 对add函数的这段代码进行编译(创建该函数的执行上下文环境和可执行代码)
- 执行add函数,输出结果
在执行add函数时,会存在两个执行上下文,一个是全局执行上下文,一个是add函数的执行上下文。
那么JS引擎是怎么管理多个执行上下文的呢,JS引擎是通过栈来管理这些执行上下文的。
# 3.一个递归造成栈溢出的例子
下面是一个递归爆炸的例子:
function test(n) {
if (n === 0) return true
return test(n - 1)
}
2
3
4
当 n 为 100 时,运行时很快输出 true,
当 n 为 10000000 时,会抛出错误 VM9657:2 Uncaught RangeError: Maximum call stack size exceeded
当JavaScript引擎开始执行这段代码时,它首先调用函数test,并创建执行上下文,压入栈中;
因为这个函数是递归的,所以它会一直创建新的函数执行上下文,并反复将其压入栈中,直到 n<=1 ,但栈是有容量限制的,超过最大数量后就会出现了栈溢出的错误。
理解了栈溢出原因后,你就可以使用一些方法来避免或者解决栈溢出的问题,比如把递归调用的形式改造成尾递归
# 4.总结
- 每调用一个函数,JavaScript引擎会为其创建执行上下文,并把该执行上下文压入调用栈,然后JavaScript引擎开始执行函数代码。
- 如果在一个函数A中调用了另外一个函数B,那么JavaScript引擎会为B函数创建执行上下文,并将B函数的执行上下文压入栈顶。
- 当前函数执行完毕后,JavaScript引擎会将该函数的执行上下文pop出栈。
- 当分配的调用栈空间被占满时,会引发“堆栈溢出”问题。
# 二、内存泄露
申请的内存执行完之后没有及时的清理和销毁,占用空闲内存,既不能使用也不能回收。 几种会导致内存泄露的情况:
- 全局变量满天飞
- 没销毁的计时器或回调函数
- 没释放的 DOM 的引用
- 闭包使用不当
# 解决方式
减少不必要的全局变量,使用 Javascript 严格模式来避免创建意外的全局变量
每次使用 setTimeout 和 setInterval, 都要注意在适当的时机手动销毁
使用完数据之后,及时解除引用(闭包中的变量、 DOM 引用)
bad
<div id="root">
<div class="child">child</div>
<button>remove</button>
</div>
<script>
let btn = document.querySelector('button')
let child = document.querySelector('.child')
let root = document.querySelector('#root')
btn.addEventListener('click', function() {
root.removeChild(child) //虽然该.child节点确实从dom中被移除了,但全局变量child仍然对该节点有引用,导致该节点的内存一直无法释放
})
</script>
2
3
4
5
6
7
8
9
10
11
12
good
<div id="root">
<div class="child">child</div>
<button>remove</button>
</div>
<script>
let btn = document.querySelector('button')
btn.addEventListener('click', function() {
let child = document.querySelector('.child')
let root = document.querySelector('#root')
//当移除节点并退出回调函数的执行上下文后会自动清除对该节点的引用
root.removeChild(child)
})
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14