javascript递归产生的报错(栈溢出)

1,893 阅读2分钟

什么是递归?

一句话来讲就是自身调用自身, 如:


/**
* 累计方法
* @params {number} num 累计的数字
* @return {number} 累计结果值
*/
function f(num) {
    if (num === 1) {
        return 1
    }
    return num + f(num - 1)
}

// 调用
f(100); // 5050 - 意思理解为 100+99+98+...+1;

递归产生的副作用

我们知道,函数调用会在内存形成一个"调用记录",又称"调用帧"(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用记录上方,还会形成一个B的调用记录。等到B运行结束,将结果返回到A,B的调用记录才会消失。如果函数B内部还调用函数C,那就还有一个C的调用记录栈,以此类推。所有的调用记录,就形成一个"调用栈"(call stack)。 —— 阮一峰

上面这段话是阮一峰老师关于尾调用优化的文章里面的一段话,我觉得用来解释递归的副作用同样合理。所以上面的方法会在以下情况报错:


f(100000);
// Maximun call stack size exceeded

解决办法

根据阮一峰老师的话,我们就可以对f方法进行改造, 思路是f执行时,不在调用自身或其他方法,这样就只保留一次调用帧,执行完也被垃圾回收机制回收


/**
* 累计方法
* @params {number} num 累计的数字
* @params {number} total 每次累计的结果
* @return {number} 累计结果值
*/
function f(num, total = 0) {
    if (num === 0) {
        return total
    }
    return () => {
        return f(num - 1, total + num)
    }
}

/**
改造成上面方法后,因为执行一次,只放回一个方法(没有立即调用)
所以f方法调用后,这个调用记录就被删除掉了
要实现上面的累加功能,还需要另一个方法配合实现
*/ 

/**
* 弹床
* @params {function} fn 需要递归的方法
* @params {number} num 这里是累计次数
* @return {number} 累计结果值
*/
function trampoline(fn, num) {
    let value = fn(num)
    // 如果每次执行fn都返回一个function,则继续执行,直到返回一个数字
    while(typeof value === 'function') {
        value = value()
    }
    return value
}

 trampoline(f, 100000); // 不会报错

这里也有参考网上的大神的例子的简化+自己总结 ——bye