函数递归调用
在程序中,所谓的递归调用就是函数直接调用自己或者间接调用自己
需注意的是,递归一定要有个结束自己调用自己的出口。
我们先看个例子:求1 到100的和
// 常规的写法
var sum = 0,
n = 100
for (var i = 1; i <= n; i++) {
sum += i
}
console.log(sum)
我们可以用递归实现来实现:
// 用递归实现。函数调用函数自己。
function sumNum(num) {
// 递归一定要有个结束自己调用自己的出口。
if (num <= 1) {
return num // 函数只要执行到return当前函数立即结束执行。
}
// 实现函数调用函数自己。
return sumNum(num - 1) + num
// arguments.callee在严格模式下回报错,所以不要这么用
// return arguments.callee(num -1) + num;
}
console.log(sumNum(100))
我们再看看个例子:求f(n)的斐波那契数列的值。
// f(n) = f(n-1) + f(n-2); n>=3
// f(0) = 0, f(1) = 1 f(2) = 1, f(3) = 2....f(4)= 3 f(5) = 5 f(6)= 8
function fibonacci(n) {
if (n == 1) {
return 1
}
if (n == 0) {
return 0
}
return fibonacci(n - 1) + fibonacci(n - 2)
}
console.log(fibonacci(6))
递归的缺点
递归函数的优点是定义简单,逻辑清晰。理论上,所有的递归函数都可以写成循环的方式,但循环的逻辑不如递归清晰。
使用递归函数需要注意防止栈溢出。在计算机中,函数调用是通过栈(stack)这种数据结构实现的,每当进入一个函数调用,栈就会加一层栈帧,每当函数返回,栈就会减一层栈帧。由于栈的大小不是无限的,所以,递归调用的次数过多,会导致栈溢出。
尾递归优化
解决递归调用栈溢出的方法是通过尾递归优化,事实上尾递归和循环的效果是一样的,所以,把循环看成是一种特殊的尾递归函数也是可以的。
尾递归是指,在函数返回的时候,调用自身本身,并且,return语句不能包含表达式。这样,编译器或者解释器就可以把尾递归做优化,使递归本身无论调用多少次,都只占用一个栈帧,不会出现栈溢出的情况。
function f() {
let m = 1;
let n = 2;
return g(m + n);
}
f();
// 等同于
function f() {
return g(3);
}
f();
// 等同于
g(3);
上面代码中,如果函数g不是尾调用,函数f就需要保存内部变量m和n的值、g的调用位置等信息。但由于调用g之后,函数f就结束了,所以执行到最后一步,完全可以删除 f() 的调用记录,只保留 g(3) 的调用记录。
这就叫做"尾调用优化"(Tail call optimization),即只保留内层函数的调用记录。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用记录只有一项,这将大大节省内存。这就是"尾调用优化"的意义。