讲一下下面的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) 时,程序并不会立即计算出结果,而是不断深入调用,直到触达终止条件。这个过程就像剥洋葱一样,一层层向内:
f(10)需要知道f(9)的结果 → 等待f(9)f(9)需要知道f(8)的结果 → 等待f(8)f(8)需要知道f(7)的结果 → 等待f(7)...以此类推...
f(2)需要知道f(1)的结果 → 等待f(1)f(1)触发终止条件num <= 1→ 返回 1
此时,调用栈达到了最深处,开始进入“归”的过程。
3. “归”的过程(向上计算)
一旦 f(1) 返回了 1,之前所有等待结果的函数开始依次获得返回值并进行计算:
f(1)返回 1f(2)得到f(1)的结果,计算1 * 2= 2,返回 2f(3)得到f(2)的结果,计算2 * 3= 6,返回 6f(4)得到f(3)的结果,计算6 * 4= 24,返回 24f(5)得到f(4)的结果,计算24 * 5= 120,返回 120f(6)得到f(5)的结果,计算120 * 6= 720,返回 720f(7)得到f(6)的结果,计算720 * 7= 5040,返回 5040f(8)得到f(7)的结果,计算5040 * 8= 40320,返回 40320f(9)得到f(8)的结果,计算40320 * 9= 362880,返回 362880f(10)得到f(9)的结果,计算362880 * 10= 3628800,返回最终结果
4. 最终输出
alert 弹窗显示的内容为:10的阶乘是:3628800
总结图示
你可以将这个过程的数学表达式理解为:
注意:虽然结果正确,但在 JavaScript 中,如果递归层级过深(例如计算 10000 的阶乘),可能会导致“栈溢出”(Stack Overflow)错误,因为每次函数调用都会在内存栈中占据空间。对于简单的阶乘计算,使用 for 循环通常性能更好且更安全。
讲解二:
这段代码展示的是经典的递归(Recursion)实现阶乘的过程。为了让你更透彻地理解,我们可以将这个过程拆解为两个阶段:“递”(向下调用)和“归”(向上返回),并结合 JavaScript 引擎内部的执行上下文栈(Call Stack)机制来解释。
1. 核心概念:什么是递归?
递归就是函数在内部调用自己。它必须包含两个关键部分:
基准条件(Base Case):即停止调用的条件。在代码中是
if(num <= 1) return 1;。如果没有这个条件,函数会无限调用自身,导致“栈溢出”错误。递归步骤(Recursive Step):将大问题分解为小问题。在代码中是
return f(num-1) * num;。意思是:“要算出num的阶乘,我需要先知道num-1的阶乘,然后乘以num”。
2. 详细执行流程图解(以 f(3) 为例)
假设我们调用 f(3),JavaScript 引擎会按照以下步骤执行:
第一阶段:“递” —— 压栈过程(Going Down)
当函数被调用时,JS 引擎会为每次调用创建一个新的执行上下文,并将其压入调用栈。此时,当前的函数执行会暂停,等待内部调用的结果。
调用
f(3):检查
3 <= 1?否。执行
return f(2) * 3。关键点:为了计算这个表达式,必须先得到
f(2)的结果。所以f(3)暂停,f(2)被压入栈顶。调用
f(2):检查
2 <= 1?否。执行
return f(1) * 2。关键点:必须先得到
f(1)的结果。所以f(2)暂停,f(1)被压入栈顶。调用
f(1):检查
1 <= 1?是!触发基准条件,直接 返回 1。
关键点:这是递归的终点(触底)。
f(1)的执行上下文从栈中弹出。
此时调用栈的状态变化:
[全局上下文]->[f(3)]->[f(2)]->[f(1)](触底,开始返回)
第二阶段:“归” —— 出栈过程(Coming Back)
一旦最内层的函数返回了值,外层暂停的函数就会拿到这个值,继续完成它们未完成的计算,然后依次返回。
回到
f(2):f(1)返回了1。f(2)继续执行:return 1 * 2。计算结果为 2。
f(2)执行完毕,从栈中弹出,将结果2返回给调用者f(3)。回到
f(3):f(2)返回了2。f(3)继续执行:return 2 * 3。计算结果为 6。
f(3)执行完毕,从栈中弹出,将结果6返回给最初的全局调用。最终结果:
alert接收到6,弹出提示框。
数学表达式视角:
3. 回到你的问题:f(10) 是如何运行的?
对于 f(10),过程完全一样,只是层级更深:
层层深入:
f(10)等f(9)->f(9)等f(8)-> ... ->f(2)等f(1)。
此时,内存中的调用栈里堆叠了 10 个函数的执行上下文(加上全局上下文共 11 个)。触底反弹:
f(1)返回1。层层计算:
修正计算链:
f(2)拿到 1,算出 ,返回 2。f(3)拿到 2,算出 ,返回 6。f(4)拿到 6,算出 ,返回 24。...
f(9)拿到 403,200,算出 ,返回该值。f(10)拿到 3,628,800,算出 (注:此处应为 3,628,800,前一步 f(9) 是 362,880,)。
4. 为什么要注意递归深度?
在 JavaScript 中,调用栈的大小是有限的。
如果你调用
f(10),栈里只有 10 层,非常安全。如果你调用
f(10000),栈里会有 10,000 层上下文。这可能会超过浏览器的最大调用栈限制,导致报错:RangeError: Maximum call stack size exceeded
总结
这段代码的递归过程就像剥洋葱:
进去的时候(递):一层层剥开,把每一层的状态(当前的
num值和等待计算的乘法操作)保存在栈里,直到看到核心(num <= 1)。出来的时候(归):从核心开始,利用内层返回的结果,结合当前层保存的状态进行计算,一层层把结果传回最外层。