Javascript进阶之栈溢出

什么是栈帧?什么是栈溢出?

**栈帧(Stack Frame)**是内存中用于存储函数调用信息的区域。每当一个函数被调用时,系统都会为其分配一块内存来存储函数的局部变量、参数、返回地址等信息,并将当前运行的函数的栈帧压入栈中,以便于后续函数调用时能够恢复之前的运行状态。当函数调用结束后,栈帧也会被弹出栈,释放内存。
举个例子:


function C() {
      // 函数C的逻辑代码
      //...
    };

function B() {
      // ...
      C(); // 调用函数C
};

function A() {
    // ...
    B(); // 调用函数B
};

A(); // 调用函数A

上面的例子中A函数调用了B函数,B函数又调用了C函数,那么首先给A函数分配栈帧,当A调用B时,A并没有结束,所以A的栈帧必须保留,系统会将A的栈帧压入(push)栈中,以便于后续返回时能够恢复之前的运行状态。接下来运行B函数,系统会为B函数分配栈帧,当B调用C时,系统会将当前函数B的栈帧压入栈中。最后运行C函数,系统会为C函数分配栈帧,并压入栈中,当C函数调用结束后,系统会弹出(pop)栈,释放C的栈帧,流程返回到B函数,B函数结束后,系统会弹出栈,释放B的栈帧,流程回到A函数,A函数结束后,系统会最后弹出栈,释放A的栈帧。

了解这个过程后我们自然想到,系统的栈空间是受限于资源的,如果函数调用层次过深,栈空间就会被占满,会怎么样呢?这就是栈溢出(Stack Overflow)的问题,我们经常听到的运维小伙伴口中的爆栈,就是栈溢出的别名。知名的程序员debug问答社区stackoverflow.com,它的名字就来源于此,可见爆栈给程序员们留下的心理阴影面积有多大。

Javascript中的栈溢出

很自然的,我们会想到,递归这种策略是很容易形成很深的调用栈的,我们可以做个实验看看JS中的栈溢出情况:


function recursive(n) {
    if (n <= 0) {
        return 0;
    }
    else return 1 + recursive(n-1);
}

recursive(50000);

上面是一个很无聊的函数,递归调用自己,直到n为0,但是这样的递归调用会导致栈溢出,因为调用50000次函数,大大超出了主流引擎的最大调用栈,系统会抛出栈溢出的异常,报错通常是这样的:


Uncaught RangeError: Maximum call stack size exceeded

JS中如何避免栈溢出?以迭代替代递归为例

这个话题太大了,我们这里只谈谈最简单的,上文提到的由于深度递归导致的栈溢出,解决方案也很简单,把递归改成迭代(循环)即可。
用例子说话:


//递归版本,相加函数
const addRecursive = x => y => y<=0? x : addRecursive(x+1)(y-1);

//循环版本,相加函数
const addLoop = x => y => {
    while(y>0){
        x++;
        y--;
        } 
    return x;
};

try{
    console.log(addRecursive(1)(50000)); // 栈溢出
}catch(e){
    console.log(e.message); // 报错:Maximum call stack size exceeded
}

console.log(addLoop(1)(50000)); // 正常运行,输出50001


以上展示了两个功能等价的函数addRecursiveaddLoop,它们的作用都是计算两个正整数的和,但是addRecursive使用递归算法,而addLoop使用循环。
addRecursive函数的实现很简单,就是递归调用自己,直到y为0,然后返回x。但是由于递归调用层次过深,会导致栈溢出。
addLoop函数的实现也很简单,就是用一个循环,对x执行后继操作(++)和对y执行前继操作(–),直到y为0,然后返回x。这个实现不会导致栈溢出,因为它不会无限调用自己,循环的开销远远小于递归调用。
这个简单的例子展示了迭代代替递归的策略,可以有效避免栈溢出。在设计函数的时候,可以参考此策略。

其它避免栈溢出的方法还有很多,比如:尾递归优化记忆化分治策略惰性求值使用迭代器和生成器等等,这些方法都有其适用的场景,我们以后再聊。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值