概述
这是我在学习函数式编程的时候,关于递归,尾递归,相互递归和蹦床函数的一些心得,记下来供以后开发时参考,相信对其他人也有用。
参考资料:JavaScript玩转Clojure大法之 - Trampoline
递归
我们知道,es5是没有尾递归优化的,所以在递归的时候,如果层数太多,就会报“Maximum call stack size exceeded”的错误。就连下面这个及其简单的递归函数都会报“Maximum call stack size exceeded”的错误。
function haha(a) {
if(!a) return a;
return haha(a-1);
}
haha(100); //输出0
haha(12345678); //输出“Maximum call stack size exceeded”
为什么会报“Maximum call stack size exceeded”的错误?我觉得原因是在每次递归调用的时候,会把当前作用域里面的基本类型的值推进栈中,所以一旦递归层数过多,栈就会溢出,所以会报错。
注意:
- js中的栈只会储存基本类型的值,比如:number, string, undefined, null, boolean。
- 为什么在调用下一层递归函数的时候没有释放上一层递归函数的作用域?因为在回来的时候还需要用到里面的变量。
尾递归
怎么优化上面的情况呢?方法是使用尾递归。有尾递归优化的编译器会把尾递归编译成循环的形式,如果没有尾递归优化,那就自己写成循环的形式。如下面的例子所示:
//尾递归函数,返回一个函数调用,并且这个函数调用是自己
function haha(a, b) {
if(b) return b;
return haha(a, a-1);
}
//优化成循环的形式
function yaya(a) {
let b = a;
while(b) {
b = b - 1;
}
}
需要注意的是,看上面尾递归的代码,有一点很重要,就是用一个b变量来存上一次递归的值。这是尾递归常用的方法。另外,其实上面尾递归的代码不需要变量b,但为了便于说明,所以我加上了变量b。
相互递归
但是关于递归还有一种形式,就是相互递归,如下面的代码所示:
function haha1(a) {
if(!a) return a;
return haha2(a-1);
}
function haha2(a) {
if(!a) return a;
return haha1(a-1);
}
haha1(100); //输出0
haha1(12345678); //输出Maximum call stack size exceeded
可以看到,当相互递归层数过多的时候,也会发生栈溢出的情况。
蹦床函数
蹦床函数就是解决上面问题的方法。
首先我们改写上面的相互递归函数:
function haha1(a) {
if(!a) return a;
return function() {
return haha2(a-1);
}
}
function haha2(a) {
if(!a) return a;
return function() {
return haha1(a-1);
}
}
这个改写就是建立一个闭包来封装相互递归的函数,它的好处是由于不是直接的相互递归调用,所以不会把上一次的递归作用域推进栈中,而是把封装函数储存在堆里面,利用堆这个容量更大但读取时间更慢的储存形式来替代栈这个容量小但读取时间快的储存形式,用时间来换取空间。
我们尝试使用一下上面的函数:
haha1(3)(); //输出一个函数
haha1(3)()()(); //输出0
通过上面的示例可以看到,如果参数不是3而是很大的一个数字的时候,我们就需要写很多个括号来实现调用很多次。为了简便,我们可以把这种调用形式写成函数,这就是蹦床函数。如下所示:
function trampoline(func, a) {
let result = func.call(func, a);
while(typeof result === 'function') {
result = result();
}
return result;
}
基本原理是一直调用,直到返回值不是一个函数为止。
最后来使用蹦床函数:
trampoline(haha1, 12345678); //过一会儿就输出0
由于储存在堆中,所以耗时较长,过了一会儿才会输出0,但是并没有报栈溢出的错误。