尾递归的调用和柯里化——————函数式编程

尾调用是函数式编程的一个重要概念,本身非常简单,一句话就是某个函数的最后一步是调用另一个函数(仅仅调用第一个函数,不用有任何其他操作,否则不属于尾递归)

1.尾递归不一定出现在函数尾部,只要是最后一步操作即可

function f(x) {
    
    if(x>0){
        return m(x);
    }
    return n(x);
}

上面的代码中,函数m和n都属于尾调用,因为他们都是函数f的最后一步操作。

2.递归帧和尾递归的优化

每一个我们进行递归,就会有一个"递归帧"压如栈空间,如A函数递归调用B函数,那么A函数的递归帧会压栈,直到B函数释放栈空间,所以递归是一个十分耗费内存的操作,如斐波那契数列

function fib() {
    if(n===1 || n===2){
        return 1;
    }
    
    return f(n-1)+f(n-2);
}
fib(10) // 55
fib(100) // 栈满

尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置,内部变量等信息不会再使用了,直接用内函数的调用帧取代外层函数即可,上代码

function fib(n,ca1=1,ca2=1){

    if(n===1 || n===2){
        return ca2;
    }

    return fib(n-1,ca2,ca1+ca2);
}
console.log(fib(100)); //354224848179262000000

这就是尾递归的优化,每次执行时栈内只有一个帧,将大大的节省内存,这就是尾递归的调用和优化。
“尾递归优化"对递归操作意义重大,所以一些函数式编程语言将其写入了规格,es6就是如此,ECMAScript的实现都必须实现尾递归优化。

3.递归函数的改写和柯里化(currying)

尾递归的实现往往需要改写递归函数,确保最后一步只调用自身。做到这一点,就要把所有用到的内部变量改写成函数的参数,上面已经可以看到,这么做的缺点是不太直观,有了两个方法可以解决这个问题。

  方法一,柯里化概念,就是将多参数的函数转化成为单参数的形式,其实加上其他函数,柯里化函数只处理第一个函数,其他的函数处理其他参数,举例如下

function currying(fn,n) {
    return function (m) {
        return fn.call(this,m,n);
    }
}

function tailFactorial(n,total) {
    if(n===1) return total;

    return tailFactorial(n-1,n*total);
}

const factorial = currying(tailFactorial,1);

console.log(factorial(5));

将尾递归函数tailFactorial变为接受1个参数的factorial

   方法二,使用es6的函数默认值

function factorial(n,total=1) {
    
    if(n===1) return total;
    
    return factorial(n-1,total*n);
}
factorial(5)//120

因为默认值给了1,所以调用时不用提供这个值。

4.严格模式

es6的尾调用优化,只在严格模式下开启,正常模式下是无效的,这是因为,正常模式下函数内部有两个变量,可以跟踪函数的调用栈

func.arguments:返回调用函数的参数(arguments.callee 得到函数) func.caller: 返回调用当前函数的那个函数。

function fib(n,ca1=1,ca2=1){

    if(n===1 || n===2){
        return ca2;
    }
    console.log(fib.caller);
    console.log(fib.arguments);
    return fib(n-1,ca2,ca1+ca2);
}
console.log(fib(5));
[Function]
{ '0': 5 }
[Function: fib]
{ '0': 4, '1': 1, '2': 2 }
[Function: fib]
{ '0': 3, '1': 2, '2': 3 }
5

可以看得到调用的参数越来越多。

5.自己实现尾递归的优化

我们如何在正常环境下,进行尾递归的优化呢?原理非常简单,尾递归之所以需要优化,就是调用太多造成栈溢出,那就要减少调用栈,用循环代替递归。

参考文献:

  阮一峰:es6标准入门(第三版)

原文地址:https://www.cnblogs.com/czy960731/p/9306090.html