递归有很多用法例如阶乘、二分查找等,这里以阶乘来做范例。
1.原始的递归表达式通常这样写:
function fun(num) {
if(num<=1) {
return 1;
}else {
return fun(num-1);
}
}
但是这种写法有一个问题:当函数名复制给其他的变量,然后将该函数名复制为其他值或null,执行被赋值的变量函数,则函数实效。即:
var fun2 = fun;
fun = null;
fun2();//报错
2.解决上述问题:用arguments.callee()来代替函数内部那个指向自身的函数。
function fun(num) {
if(num<=1) {
return 1;
}else {
return arguments.callee(num-1);
}
}
缺点:但是在严格模式下,脚本不能访问arguments.callee,访问这个属性会出错。
3.解决上述问题:使用命名函数表达式达到相同的结目的
var fun = (function f(num) {
if(num<=1) {
return 1;
}else {
return num*f(num-1);
}
});
以上代码创建了一个名为f()的命名函数表达式,然后将它赋值给变量fun,即使把函数赋值给其他变量,函数 名字f依然有效,所以递归调用照样能完成。而且无论在严格模式还是在非严格模式下都能实现。
4、不过以上写法都不是终极优化版本,由于以上递归写法中都使用了尾调用(尾调用就是指函数作为另一个函数的最后一条语句被调用),在ES5引擎中,尾调用的实现与其他函数调用的实现类似:创建一个新的栈帧,将其推入调入栈来表示函数调用。即在循环调用中,每一个未使用完的栈帧都会被保存在内存中,当调用栈变得过大就会造成程序问题。这也是以上所有递归写法都存在一个问题。
下边这种方法是在ES6中关于尾调用优化的一种方法:es6中缩减了严格模式下尾调用栈的大小(非严格模式不影响),如果满足下列三个条件尾调用不再创建新的栈帧,而是清除并重用当前栈帧。
*尾调用不是闭包,不访问当前栈帧的变量。
*在函数内部,尾调用是最后一条语句,且不能进行其他运算操作。
*尾调用的结果作为函数值返回。
也就是下列这种形式就可被javaScript引擎自动优化:
"use strict";function fun(num,k=1) {
if(num<=1) {
return 1*k;
}else {
let result = num*k;
return fun(num,result);
}
}
在写递归函数的时候,如果递归函数的运算量足够大使用尾调用可以大幅提升程序性能。