「JS硬气功👊」我调我自己?你真的会写JavaScript递归吗?

413 阅读3分钟

Hi!这里是JustHappy!相信各位都有使用过递归吧?但是怎么说呢?似乎无论是学校的课本还是很多教程,都是直接丢给你这个概念,“对,这就是递归,就是在函数里调用函数本身”,但是?为啥呢?怎么用递归呢?我们在JavaScript中聊聊吧

image.png

何为递归?如何写一个递归?

我查了wiki百科的解释,但是感觉还是需要补充的,小弟斗胆解读一下,下面是我觉得重要的两句话

划重点!调用自身

字面意思的衍生“暴论” —— ”传递“”回归“

我并没有查阅到较为权威的文献从字面意思去解读“递归”,但是我觉得“传递”和“回归”是递归这种解决方案的特点,

所谓传递不止有参数

通常我们写递归时,除了传递参数,我们其实也在传递“状态”。

举个例子,比如我们可以使用递归去遍历一个数组:

function sum(arr, index = 0) {
  if (index === arr.length) return 0;
  return arr[index] + sum(arr, index + 1);
}

我们通过“传递参数”实现了状态的传递:

回归

const arr = [1, [2, [3, 4]], 5];

function flatten(array) {
  const result = [];

  for (const item of array) {
    if (Array.isArray(item)) {
      result.push(...flatten(item)); // 递:深入处理嵌套数组
    } else {
      result.push(item); // 终止条件:遇到数字就收集
    }
  }

  return result; // 回归:每一层把自己的结果“还”回去
}

console.log(flatten(arr)); // 输出: [1, 2, 3, 4, 5]

递归终止条件

终止条件是递归停止继续调用自身的时机,是整个递归的“出口”。

什么时候需要终止递归?

如果没递归终止条件这个出口,程序会爆栈(stack overflow) ,比如下面这个递归就永远不会停止:

function forever() {
  return forever();
}

forever(); // RangeError: Maximum call stack size exceeded

如何写递归终止条件?

✅ 方式一:索引越界
function printArray(arr, i = 0) {
  if (i >= arr.length) return;
  console.log(arr[i]);
  printArray(arr, i + 1);
}
✅ 方式二:元素类型满足某条件
function findFirstEven(arr, i = 0) {
  if (i >= arr.length) return null;
  if (arr[i] % 2 === 0) return arr[i];
  return findFirstEven(arr, i + 1);
}
✅ 方式三:结构遍历到底
function printNested(obj) {
  if (typeof obj !== 'object' || obj === null) {
    console.log(obj);
    return;
  }

  for (let key in obj) {
    printNested(obj[key]);
  }
}

printNested({ a: 1, b: { c: 2, d: { e: 3 } } });

“尾递归” 是个啥?

尾递归(Tail Recursion)指的是函数的最后一步操作是递归调用自身,并不依赖于调用的返回值进一步处理

啊哈,这名字听起来有点技术含量(PS:不说人话),其实我们可以这样理解它:

尾递归 = 递归发生在函数的最后一步 (具体我们可以对比着普通递归说明)

普通递归(需要回溯,栈层层叠加):
function factorial(n) {
  if (n <= 1) return 1;
  return n * factorial(n - 1); // 递归调用后的结果还要参与乘法
}

栈调用结构:

factorial(5)
=> 5 * factorial(4)
=> 5 * 4 * factorial(3)
=> ...
✅ 尾递归(直接返回,无需“回头处理”):
function factorialTR(n, acc = 1) {
  if (n <= 1) return acc;
  return factorialTR(n - 1, acc * n); // 递归调用是最后一步
}

尾递归结构:

factorialTR(5, 1)
=> factorialTR(4, 5)
=> factorialTR(3, 20)
=> ...

如果语言支持“尾调用优化”(TCO),尾递归就不会占用额外的栈空间,能避免爆栈。

但是......

JavaScript 目前并未正式支持尾调用优化(TCO),尽管在 ES6 中有相关规范,但多数运行环境(如 V8)并没有实现。所以尾递归写法更像是一种代码风格或习惯,在某些语言如 Scheme、Haskell 中才有实质性能提升。