调用栈:为什么JS代码会出现栈溢出

133 阅读4分钟

image.png

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

一、什么是函数调用

函数调用就是运行一个函数。

function add() { 
    return 1 + 2; 
} 
add();

在执行add之前,js引擎会为这段代码创建全局执行上下文,函数保存在全局上下文的变量环境中。

image.png 函数执行过程:从全局执行上下文中,取出add函数代码,对add函数的这段代码执行编译,并创建该函数的执行上下文和可执行代码。

当执行add函数的时候,就有两个执行上下文(全局执行上下文和add函数的执行上下文)

image.png 也就是说在执行JS时,可能会存在多个执行上下文,js引擎通过栈的数据结构来管理这些执行上下文。

二、什么是JS的调用栈

JavaScript 调用栈(Call Stack)是一种用于追踪函数调用的机制,它是一个存储函数调用的栈结构,用于管理执行上下文的堆栈。每当 JavaScript 引擎执行一个函数时,会将该函数的执行上下文推入调用栈,当函数执行完毕时,对应的执行上下文会从调用栈中弹出。

  1. 执行上下文(Execution Context): 调用栈中的每个元素都是一个执行上下文,它包含了函数执行时的环境信息,比如变量、函数、this 的值等。
  2. 入栈(Push): 当一个函数被调用时,它的执行上下文被推入调用栈的顶部,成为当前正在执行的上下文。
  3. 出栈(Pop): 当函数执行完毕,对应的执行上下文会从调用栈中弹出,控制权移交给上一个上下文。
  4. 同步执行: JavaScript 是单线程的,调用栈是同步执行的,每次只能执行一个函数。当一个函数在执行过程中调用了另一个函数,新函数的执行上下文会被推入调用栈,原函数的执行会在新函数执行完毕后继续。
  5. 堆栈溢出(Stack Overflow): 如果调用栈变得过大,超过了 JavaScript 引擎的限制,就会导致堆栈溢出,通常是因为递归调用没有结束条件。

三、在开发中如何利用好调用栈

1.如何利用浏览器查看调用栈的信息

开发者工具中=>Source,选择JS代码的页面,对代码打上断点刷新页面

image.png 也可以通过console.trace()来查看当前的函数调用关系

var a = 2; 
function add() { 
    var b = 2; 
    console.trace(); 
    return a + b; 
} 

function addAll() { 
    var d = 2; 
    var res = add(); 
    return d + res; 
} 
addAll();

image.png

2.栈溢出

调用栈是有大小的,当入栈的执行上下文超过一定数目,js引擎就会报错,这种错误就是栈溢出。

在浏览器环境下,不同浏览器可能有不同的调用栈大小限制。例如,在 Chrome 浏览器中,栈溢出错误通常发生在调用栈深度达到几千层时。在其他浏览器和 JavaScript 引擎中,这个限制可能会有所不同。

四、优化调用栈的方式

1.尾调用优化(Tail Call Optimization):

尾调用是指函数的最后一个操作是调用另一个函数。一些 JavaScript 引擎支持尾调用优化,它能够在不增加调用栈深度的情况下重用当前调用栈帧。但需要注意,并非所有的 JavaScript 引擎都支持尾调用优化,而且在严格模式下也会禁用这个优化。

function optimizedFactorial(n, acc = 1) { 
    if (n <= 1) { 
        return acc; 
    } 
return optimizedFactorial(n - 1, n * acc); 
}

2.避免不必要的递归深度:

在设计递归函数时,确保递归深度合理,不会因为无限递归导致栈溢出。如果可能的话,可以考虑使用迭代替代递归,或者结合循环进行处理。

// 避免不必要的递归深度 
function factorial(n) { 
    let result = 1; 
    for (let i = 2; i <= n; i++) { 
        result *= i; 
    } 
    return result; 
}

3.避免不必要的闭包:

使用闭包会导致额外的内存消耗,并可能对性能产生影响。在一些情况下,可以考虑使用函数参数或其他方式替代闭包。

// 避免不必要的闭包 
function processArray(arr, callback) { 
    for (let i = 0; i < arr.length; i++) { 
        // 避免在这里创建不必要的闭包 
        callback(arr[i]); 
    } 
}