代码为什么会栈溢出?

88 阅读4分钟

什么是栈?

栈是一种后进先出的数据结构。

什么是调用栈?

调用栈是用来管理函数调用关系的一种栈结构。

什么是 JavaScript 的调用栈?

当我们去调用一个函数的时候会创建函数执行上下文,JavaScript 引擎会将执行上下文压入栈中,通常把这种用来管理执行上下文的栈称为执行上下文栈,又称调用栈。它是 JavaScript 引擎追踪函数执行的一个机制,当一次有多个函数被调用时,通过调用栈就能够追踪到哪个函数正在被执行以及各函数之间的调用关系。

压栈的过程?

function add(b,c){ return b+c } 
function addAll(b,c){ 
var d = 10 
result = add(b,c) return a+result+d
} 
addAll(3,6)

第一步:创建全局上下文,并将其压入栈底。然后开始执行全局上下文中的代码。

第二步:调用 addAll 函数。当调用该函数时,JavaScript 引擎会编译该函数,并为其创建一个执行上下文,最后还将该函数的执行上下文压入栈中。addAll 函数的执行上下文创建好之后,便进入了函数代码的执行阶段了,这里先执行的是d=10 的赋值操作,执行语句会将 addAll 函数执行上下文中的 d 由 undefined 变成了10。

第三步:当执行到 add 函数调用语句时,同样会为其创建执行上下文,并将其压入调用栈,当 add 函数返回时,该函数的执行上下文就会从栈顶弹出,并将 result 的值设置为 add函数的返回值。

第四步:当addAll 执行最后一个相加操作后并返回,addAll 的执行上下文也会从栈顶部弹出,此时调用栈中就只剩下全局上下文了

在开发中如何利用好调用栈?

当你执行一段复杂的代码时,你可能很难从代码文件中分析其调用关系,这时候你可以在你想要查看的函数中加入断点,然后当执行到该函数时,就可以查看该函数的调用栈了。

这么说可能有点抽象,这里我们拿上面的那段代码做个演示,你可以打开“开发者工具”,点击“Source”标签,选择 JavaScript 代码的页面,然后在第 3 行加上断点,并刷新页面。你可以看到执行到 add 函数时,执行流程就暂停了,这时可以通过右边“call stack”来查看当前的调用栈的情况

从图中可以看出,右边的“call stack”下面显示出来了函数的调用关系:栈的最底部是anonymous,也就是全局的函数入口;中间是 addAll 函数;顶部是 add 函数。这就清晰地反映了函数的调用关系,所以在分析复杂结构代码,或者检查 Bug 时,调用栈都是非常有用的。

除了通过断点来查看调用栈,你还可以使用 console.trace() 来输出当前的函数调用关系,比如在示例代码中的 add 函数里面加上了 console.trace(),你就可以看到控制台输出的结果。

为什么栈会溢出?

栈其实是有大小限制的,当入栈的执行上下文超过一定数目,JavaScript 引擎就会报错,我们把这种错误叫做栈溢出。比如我们写递归代码的时候就很容易栈溢出。

function division(a,b){ return division(a,b) } console.log(division(1,2))

当 JavaScript 引擎开始执行这段代码时,它首先调用函数 division,并创建执行上下文,压入栈中;然而,这个函数是递归的,并且没有任何终止条件,所以它会一直创建新的函数执行上下文,并反复将其压入栈中,但栈是有容量限制的,超过最大数量后就会出现栈溢出的错误。

总结

每调用一个函数,JavaScript 引擎会为其创建执行上下文,并把该执行上下文压入调用栈,然后 JavaScript 引擎开始执行函数代码。

如果在一个函数 A 中调用了另外一个函数 B,那么 JavaScript 引擎会为 B 函数创建执行上下文,并将 B 函数的执行上下文压入栈顶。

当前函数执行完毕后,JavaScript 引擎会将该函数的执行上下文弹出栈。

当分配的调用栈空间被占满时,会引发“堆栈溢出”问题。