首页/文章/八股文

在 JS 中, 如何解决递归导致栈溢出问题

2024-12-18
9074 分钟
...

关键词:栈溢出问题

在 JavaScript 中,递归如果执行过深,确实有可能导致“栈溢出(stack overflow)”错误,因为每次函数调用都会向调用栈中添加一个新的帧,而每个线程的调用栈都有其最大容量限制。当这个容量被超过时,就会发生栈溢出。为了解决这个问题,你可以使用几种不同的方法:

尾调用优化(Tail Call Optimization)

在 ES6 中,引入了尾调用优化。这意味着如果函数的最后一个操作是返回另一个函数的调用(即尾调用),那么这个调用可以在不增加新栈帧的情况下执行。但是,截至我知识更新的时间,大多数 JavaScript 引擎还没有实现这项优化,或者它在默认情况下并未激活。

转化为循环

大多数递归函数都可以重写为循环,这样可以避免调用栈问题。这种方法需要手动维护一个栈来存储必要的状态信息,而这个栈通常是存储在堆(heap)中的数组,不受调用栈大小限制。

例如,下面递归计算阶乘的代码:

function factorial(n) {
  if (n === 1) return 1;
  return n * factorial(n - 1);
}

可以重写为循环形式:

function factorial(n) {
  let result = 1;
  for (let i = 2; i <= n; i++) {
    result *= i;
  }
  return result;
}

用 Trampoline 函数

Trampoline 是一个高阶函数,使您可以在递归调用的情况下避免栈溢出。它通过在每个递归步骤返回一个函数而不是值,然后持续调用这些函数,直到获得最终结果为止。

function trampoline(fn) {
  return function (...args) {
    let result = fn.apply(this, args);

    while (typeof result === "function") {
      result = result();
    }

    return result;
  };
}

然后,将原始递归函数改写为每次递归调用返回一个函数:

function recursiveFunction(args) {
  if (baseCase) {
    return finalValue;
  } else {
    return function () {
      return recursiveFunction(newArgs);
    };
  }
}

const trampolinedFunction = trampoline(recursiveFunction);

调用 trampolinedFunction 会避免栈溢出,因为它不是真正的递归调用,只是同步循环调用那些返回的函数。

生成器和 Promises

使用 ES6 的生成器(generator)和/或 Promises 也可以用来避免递归调用过深。这些特性可以帮助您生成异步递归调用,其允许事件循环(event loop)介入,避免单次执行过多递归调用造成的栈溢出。

使用异步递归

将递归函数改造成异步函数(async function),并确保每一次递归调用都有机会返回控制权给 JavaScript 事件循环,这可以通过setTimeoutsetImmediate或者process.nextTick(在 Node.js 环境下)实现。

例如,可以将一个同步递归函数改写为:

function recursiveAsyncFunction(i) {
  if (i < 0) return Promise.resolve();
  console.log("Recursion ", i);
  return new Promise((resolve) => {
    setImmediate(() => {
      resolve(recursiveAsyncFunction(i - 1));
    });
  });
}

记得确保递归终止条件是正确的,否则即便以上方法也可能导致无限循环或者内存泄漏。每一种方法都有其适用场景,具体使用哪一种方法取决于问题的具体需求。

如果您觉得这篇文章有帮助,请点个赞吧~

分享文章

相关文章

更多文章 →
八股文2026-07-30
一、TypeScript与类型系统(20题)
1\. 在定义AI接口返回的嵌套数据结构(如多轮对话、工具调用结果)时,如何用TypeScript的泛型与条件类型实现灵活的类型推导? 场景:为“AI助手”定义统一的消息类型系统,需兼容不同模型供应商(OpenAI/Anthropic)及内部(Moonshot)的返回格式。 核心答案: 构建一个以泛型为基础、条件类型为分支的层次化类型系统。核心是利用泛型参数化“消息内容”、“工具调用”和“提供商”,再通过条件类型精确匹配不同场景下的数据...
学习
八股文2025-10-13
cookie跨域介绍
Cookie 跨域问题详解 在 Web 开发中, Cookie 是最常见的客户端存储机制之一,用于记录用户登录状态、偏好设置、会话信息等。然而,当涉及到 跨域请求 时,Cookie 的行为往往变得复杂,尤其在现代浏览器的安全策略下,跨域 Cookie 的传递和写入都有严格的限制。 本文将从基础概念开始,逐步讲解 Cookie 的跨域机制、SameSite 属性、CORS 配置及常见问题解决方案。 一、Cookie 的基础概念 Cooki...
学习面试
八股文2025-10-09
JavaScript 闭包详解
JavaScript 闭包(Closure)详解 一、什么是闭包 闭包(Closure) 是 JavaScript 中一个非常核心且常被问到的概念。简单来说: 闭包是一个函数,它可以“记住”并访问其定义时所在的词法作用域,即使这个函数在其作用域之外被调用。 换句话说,当一个函数“嵌套”在另一个函数中,并且 内部函数引用了外部函数的变量 时,就形成了闭包。 示例: 虽然 已经执行完毕,但 依然可以访问 中的 。这就是闭包。 二、闭包的形成...
学习面试
八股文2025-10-07
开始性能优化之旅
事件循环机制 一、JavaScript 引擎的本质 核心职责 : 解析 JavaScript 语法 管理变量和内存 执行代码逻辑 不涉及 : 线程管理(Worker除外) I/O 操作 定时器控制 网络请求 常见引擎:V8(Chrome)、SpiderMonkey(Firefox)、JavaScriptCore(Safari) 二、宿主环境的扩展能力 宿主提供的多线程能力 : | 线程类型 | 功能 | 对应 API | | | | |...
学习面试
八股文2025-09-30
前端首屏优化
话说,我在面试的时候,80%的情况下,都会被问到首屏优化问题,烦,恨 TCP Slow Start(慢启动)概念 咱先不说标题的数字哪里来的,先说一个概念,就是TCP的慢启动。 你们想哈,在浏览器和服务器开始建立连接的时候,服务器是并不知道浏览端网络的带宽、拥塞状况。假设你本地的带宽是1M,如果一开始服务器就发送2M的文件,那浏览器压根就不是人类,忍忍或者挤挤就能接收到的,这样就可能会引发丢包和重传。 想想,这个该有什么办法解决这个问题...
学习面试
八股文2025-09-28
Function.prototype和Object.prototype
一道面试题引发的原型链血案,彻底搞懂 Function.prototype 和 Object.prototype 前言 有这样一道题: 目录 问题分析与答案揭晓 让我们先公布正确答案,然后深入分析原因: 为什么 是 而不是 ? 这是因为函数和实例的原型链路径完全不同! 原型链的本质机制 JavaScript中的两条原型链 在JavaScript中,存在两条不同的原型链: &nbsp; &nbsp; &nbsp; 关键理解:F.proto...
学习面试

评论

请登录后发表评论

去登录
加载评论中...

目录