函数式编程之尾调用和尾递归

尾调用

前一段时间偶然状况下了解到了尾调用这个概念,而后就去了解了一下,其实用代码来解释是很是容易的:html

function foo(x){
    return x+1
}
function ex(){
    var num = 2
    return foo(num)
}
//尾调用不必定出如今函数尾部,只要是最后一步操做便可。
//当一个函数的最后一步是另外一个函数的调用(只是某个函数的调用),那么,这种状况就称之为尾调用
复制代码

尾调用为何会单独拿出来做为一个概念而且被讨论,究其缘由是函数调用会在内存中造成一个调用帧(用以保存调用位置,上下文变量等信息),若是在函数A内部调用了函数B,那么,会在A的调用帧上面再记录一个B的调用帧,等B执行结束将结果返回给A以后,B的调用帧消失;若是B内部还调用了函数C,那么,C的调用帧。还会出如今B的上方。这就是一个压栈的过程,所以调用过程实际上是造成了一个调用栈的。 尾调用因为是函数的最后一步操做,因此不须要保留外层函数的调用记录,由于调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用记录,取代外层函数的调用记录就能够了。编程

优化

优化方式根据使用的语言不一样则有不一样实现,此处拿JavaScript来举例编程语言

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),即只保留内层函数的调用记录。若是全部函数都是尾调用,那么彻底能够作到每次执行时,调用记录只有一项,这将大大节省内存。这就是"尾调用优化"的意义。函数

尾递归

尾调用的是自身,被称为尾递归。 递归很是耗费内存,由于须要同时保存成千上百个调用记录,很容易发生"栈溢出"错误(stack overflow)。但对于尾递归来讲,因为只存在一个调用记录,因此永远不会发生"栈溢出"错误。优化

function factorial(n) {
  if (n === 1) return 1;
  return n * factorial(n - 1);
}

factorial(5) // 120
复制代码

上面代码是一个阶乘函数,计算n的阶乘,最多须要保存n个调用记录,复杂度 O(n) 。spa

若是改写成尾递归,只保留一个调用记录,复杂度 O(1) 。code

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

factorial(5, 1) // 120
复制代码

因而可知,"尾调用优化"对递归操做意义重大,因此一些函数式编程语言将其写入了语言规格。ES6也是如此,第一次明确规定,全部 ECMAScript 的实现,都必须部署"尾调用优化"。这就是说,在 ES6 中,只要使用尾递归,就不会发生栈溢出,相对节省内存,ES6的尾调用优化只在严格模式下开启,正常模式是无效的。(参考自阮一峰:www.ruanyifeng.com/blog/2015/0…)htm

相关文章
相关标签/搜索