你以为斐波那契只是递归题?不——它是 JavaScript 性能分水岭

92 阅读4分钟

每一个前端,在学递归时,都被斐波那契“温柔地折磨”过。

今天,我们不只写代码,而是彻底看懂

  • 为什么最经典的递归斐波那契会慢到怀疑人生?
  • 什么是“用空间换时间”的真正含义?
  • 为什么说 闭包 + IIFE 是 JavaScript 里实现记忆函数的“王炸组合”?

这篇文章,适合:

  • 刚学递归、总被复杂度吓到的你
  • 面试前想把“记忆化”真正讲清楚的你
  • 想写出更“有 JS 味道”代码的你

一、斐波那契:看起来很简单,实际上很危险

先从最经典的写法开始:

function fib(n) {
  if (n <= 1) return n;
  return fib(n - 1) + fib(n - 2);
}

fib(10); // 55

这段代码有三个优点:

  • ✅ 数学公式一一对应
  • ✅ 逻辑直观、非常优雅
  • ✅ 递归思想体现得淋漓尽致

但它也有一个致命问题

时间复杂度是 O(2^n)

当你执行 fib(40) 时,电脑并不是“算慢了”,而是在:

  • 一遍遍重复计算相同的子问题
  • 疯狂创建函数调用栈
  • 在指数级的调用树里迷路

二、递归为什么会“爆炸”?

我们以 fib(5) 为例:

b6a3b5b60f4d12a9ed8cfaed9ffb2daa.png

你会发现:

  • fib(3) 被算了 2 次
  • fib(2) 被算了 3 次
  • fib(1) 被算了 2 次

问题不在递归,而在:

递归 + 重复子问题 = 性能灾难


三、第一层优化:用空间换时间

既然问题是“重复计算”,那思路就很清晰了:

算过的结果,存起来。

const cache = {};

function fib(n) {
  if (n in cache) return cache[n];

  if (n <= 1) {
    cache[n] = n;
    return n;
  }

  const result = fib(n - 1) + fib(n - 2);
  cache[n] = result;
  return result;
}

fib(100);

🎉 奇迹发生了:

  • 时间复杂度 ➜ O(n)
  • 调用次数大幅减少
  • 性能直接起飞

但这里有个问题:

cache 是全局变量

在真实项目中,这通常意味着:

  • 可能被意外修改
  • 不利于封装
  • 不够“优雅”

于是,我们需要下一步进化。


四、记忆函数的终极形态:闭包

如果你学过闭包,一定听过一句话:

闭包 = 函数 + 它能访问的自由变量

那我们能不能让 cache

  • 不暴露在全局
  • 又能被多次调用记住

答案是:可以,而且非常 JS。


五、IIFE:只执行一次的“外壳”

先引入一个工具:

IIFE(Immediately Invoked Function Expression)

(function () {
  // 定义后立刻执行
})();

它的特点非常适合做三件事:

  • ✅ 创建私有作用域
  • ✅ 初始化一次性逻辑
  • ✅ 生成闭包

六、闭包 + IIFE:记忆型斐波那契完整版

const fib = (function () {
  const cache = {};

  return function (n) {
    if (n in cache) return cache[n];

    if (n <= 1) {
      cache[n] = n;
      return n;
    }

    cache[n] = fib(n - 1) + fib(n - 2);
    return cache[n];
  };
})();

fib(100);

我们来逐层拆解它。


七、这段代码“牛”在哪?

1️⃣ cache 被闭包保护

  • cache 不在全局
  • 外界无法直接访问或修改
  • 但每次调用 fib 都能共享它

这正是闭包的精髓:

函数执行完了,但它的作用域没有消失


2️⃣ IIFE 只执行一次

const fib = (function () {
  console.log('只执行一次');
  return function () {};
})();
  • 初始化逻辑只跑一次
  • cache 只创建一次
  • 后续全是高效复用

3️⃣ 递归依然存在,但不再“浪费”

递归没有错,错的是:

  • 不加控制的重复递归

记忆化让递归从:

指数级灾难 ➜ 线性可控


八、这其实是一个通用模式

你现在学到的,不只是斐波那契。

这是一个通用的记忆函数模板

function memo(fn) {
  const cache = {};
  return function (key) {
    if (key in cache) return cache[key];
    return cache[key] = fn(key);
  };
}

很多库、很多面试题,本质都在考这个思想。


九、什么时候该用这种写法?

非常适合:

  • 有大量重复子问题
  • 输入 ➜ 输出稳定
  • 纯函数或近似纯函数

例如:

  • 递归算法
  • 复杂计算缓存
  • 前端性能优化

十、一句话总结

递归决定思路,缓存决定性能,闭包决定优雅。

如果你能把“斐波那契 + 闭包 + IIFE”讲清楚:

  • 你已经超过了大多数只会背概念的候选人
  • 你也真正理解了 JavaScript 的执行模型

如果这篇文章对你有帮助,欢迎点赞、收藏、评论 👏