您好!欢迎访问家园网-www.jy.wang!

家园网

详细讲解Javascript的递归函数

网络 作者:本站 点击:

讲一下下面的js代码的10的阶乘是如何循环的?   

<script>
        function f(num){
            if(num<=1){
                return 1;
            }else{
                return f(num-1)*num;
            }
        }
        alert("10的阶乘是:"+f(10));
    </script>


讲解一:


这段代码实际上使用的不是“循环”(Loop),而是‌递归‌(Recursion)。

递归的核心思想是:‌函数在执行过程中调用自身‌,将一个大问题分解为规模更小的子问题,直到满足终止条件后,再层层返回结果。

以下是 f(10) 计算 10 的阶乘的具体执行过程解析:

1. 核心逻辑分析

代码定义了一个函数 f(num)

  • 终止条件(基线条件)‌:if(num <= 1) return 1;

    • 当数字减小到 1 或 0 时,不再继续调用自身,直接返回 1。这是防止无限递归的关键。

  • 递归步骤‌:return f(num-1) * num;

    • 当前数字 num 乘以 num-1 的阶乘结果。

    • 为了得到 num-1 的阶乘,必须再次调用 f(num-1),从而形成链式调用。

2. “递”的过程(向下拆解)

当调用 f(10) 时,程序并不会立即计算出结果,而是不断深入调用,直到触达终止条件。这个过程就像剥洋葱一样,一层层向内:

  1. f(10) 需要知道 f(9) 的结果 → 等待 f(9)

  2. f(9) 需要知道 f(8) 的结果 → 等待 f(8)

  3. f(8) 需要知道 f(7) 的结果 → 等待 f(7)

  4. ...以此类推...

  5. f(2) 需要知道 f(1) 的结果 → 等待 f(1)

  6. f(1) 触发终止条件 num <= 1 → ‌返回 1

此时,调用栈达到了最深处,开始进入“归”的过程。

3. “归”的过程(向上计算)

一旦 f(1) 返回了 1,之前所有等待结果的函数开始依次获得返回值并进行计算:

  1. f(1) 返回 ‌1

  2. f(2) 得到 f(1) 的结果,计算 1 * 2 = ‌2‌,返回 2

  3. f(3) 得到 f(2) 的结果,计算 2 * 3 = ‌6‌,返回 6

  4. f(4) 得到 f(3) 的结果,计算 6 * 4 = ‌24‌,返回 24

  5. f(5) 得到 f(4) 的结果,计算 24 * 5 = ‌120‌,返回 120

  6. f(6) 得到 f(5) 的结果,计算 120 * 6 = ‌720‌,返回 720

  7. f(7) 得到 f(6) 的结果,计算 720 * 7 = ‌5040‌,返回 5040

  8. f(8) 得到 f(7) 的结果,计算 5040 * 8 = ‌40320‌,返回 40320

  9. f(9) 得到 f(8) 的结果,计算 40320 * 9 = ‌362880‌,返回 362880

  10. f(10) 得到 f(9) 的结果,计算 362880 * 10 = ‌3628800‌,返回最终结果

4. 最终输出

alert 弹窗显示的内容为:
10的阶乘是:3628800

总结图示

你可以将这个过程的数学表达式理解为:

f(10)=10×f(9)=10×(9×f(8))=10×9×(8×f(7))...=10×9×8×7×6×5×4×3×2×f(1)=10×9×8×7×6×5×4×3×2×1=3,628,800

注意‌:虽然结果正确,但在 JavaScript 中,如果递归层级过深(例如计算 10000 的阶乘),可能会导致“栈溢出”(Stack Overflow)错误,因为每次函数调用都会在内存栈中占据空间。对于简单的阶乘计算,使用 for 循环通常性能更好且更安全。



讲解二:


这段代码展示的是经典的‌递归(Recursion)‌实现阶乘的过程。为了让你更透彻地理解,我们可以将这个过程拆解为两个阶段:‌“递”(向下调用)‌和‌“归”(向上返回)‌,并结合 JavaScript 引擎内部的‌执行上下文栈(Call Stack)‌机制来解释。

1. 核心概念:什么是递归?

递归就是函数在内部调用自己。它必须包含两个关键部分:

  1. 基准条件(Base Case)‌:即停止调用的条件。在代码中是 if(num <= 1) return 1;。如果没有这个条件,函数会无限调用自身,导致“栈溢出”错误。

  2. 递归步骤(Recursive Step)‌:将大问题分解为小问题。在代码中是 return f(num-1) * num;。意思是:“要算出 num 的阶乘,我需要先知道 num-1 的阶乘,然后乘以 num”。


2. 详细执行流程图解(以 f(3) 为例)

假设我们调用 f(3),JavaScript 引擎会按照以下步骤执行:

第一阶段:“递” —— 压栈过程(Going Down)

当函数被调用时,JS 引擎会为每次调用创建一个新的‌执行上下文‌,并将其压入‌调用栈‌。此时,当前的函数执行会暂停,等待内部调用的结果。

  1. 调用 f(3)‌:

    • 检查 3 <= 1?否。

    • 执行 return f(2) * 3

    • 关键点‌:为了计算这个表达式,必须先得到 f(2) 的结果。所以 f(3) 暂停,‌f(2) 被压入栈顶‌。

  2. 调用 f(2)‌:

    • 检查 2 <= 1?否。

    • 执行 return f(1) * 2

    • 关键点‌:必须先得到 f(1) 的结果。所以 f(2) 暂停,‌f(1) 被压入栈顶‌。

  3. 调用 f(1)‌:

    • 检查 1 <= 1?‌是!

    • 触发基准条件,直接 ‌返回 1‌。

    • 关键点‌:这是递归的终点(触底)。f(1) 的执行上下文从栈中弹出。

此时调用栈的状态变化:
[全局上下文] -> [f(3)] -> [f(2)] -> [f(1)] (触底,开始返回)


第二阶段:“归” —— 出栈过程(Coming Back)

一旦最内层的函数返回了值,外层暂停的函数就会拿到这个值,继续完成它们未完成的计算,然后依次返回。

  1. 回到 f(2)‌:

    • f(1) 返回了 1

    • f(2) 继续执行:return 1 * 2

    • 计算结果为 ‌2‌。

    • f(2) 执行完毕,从栈中弹出,将结果 2 返回给调用者 f(3)

  2. 回到 f(3)‌:

    • f(2) 返回了 2

    • f(3) 继续执行:return 2 * 3

    • 计算结果为 ‌6‌。

    • f(3) 执行完毕,从栈中弹出,将结果 6 返回给最初的全局调用。

  3. 最终结果‌:

    • alert 接收到 6,弹出提示框。

数学表达式视角:

f(3)=3×f(2)=3×(2×f(1))=3×(2×1)=6

3. 回到你的问题:f(10) 是如何运行的?

对于 f(10),过程完全一样,只是层级更深:

  1. 层层深入‌:
    f(10) 等 f(9) -> f(9) 等 f(8) -> ... -> f(2) 等 f(1)
    此时,内存中的调用栈里堆叠了 10 个函数的执行上下文(加上全局上下文共 11 个)。

  2. 触底反弹‌:
    f(1) 返回 1

  3. 层层计算‌:

    修正计算链:

    • f(1)=1

    • f(2)=2

    • f(3)=6

    • f(4)=24

    • f(5)=120

    • f(6)=720

    • f(7)=5040

    • f(8)=40320

    • f(9)=362880

    • f(10)=362880×10=3,628,800

    • f(2) 拿到 1,算出 1×2=2,返回 2。

    • f(3) 拿到 2,算出 2×3=6,返回 6。

    • f(4) 拿到 6,算出 6×4=24,返回 24。

    • ...

    • f(9) 拿到 403,200,算出 403,200×9=3,628,800,返回该值。

    • f(10) 拿到 3,628,800,算出 3,628,800×10=36,288,000(注:此处应为 3,628,800,前一步 f(9) 是 362,880,362,880×10=3,628,800)。

4. 为什么要注意递归深度?

在 JavaScript 中,调用栈的大小是有限的。

  • 如果你调用 f(10),栈里只有 10 层,非常安全。

  • 如果你调用 f(10000),栈里会有 10,000 层上下文。这可能会超过浏览器的最大调用栈限制,导致报错:
    RangeError: Maximum call stack size exceeded

总结

这段代码的递归过程就像‌剥洋葱‌:

  1. 进去的时候‌(递):一层层剥开,把每一层的状态(当前的 num 值和等待计算的乘法操作)保存在栈里,直到看到核心(num <= 1)。

  2. 出来的时候‌(归):从核心开始,利用内层返回的结果,结合当前层保存的状态进行计算,一层层把结果传回最外层。


标签: