尾调用优化

什么是尾调用

  • 尾调用(Tail Call)是函数式编程的一个重要概念,就是指某个函数的最后一步是调用另一个函数。
// 尾调用
function f(x){
  return g(x);
}
// 不属于尾调用, 调用函数g之后,还有赋值操作
function f(x){
  let y = g(x);
  return y;
}

// 不属于尾调用, 调用后还有操作,即使写在一行内
function f(x){
  return g(x) + 1;
}

// 不属于尾调用,下面的两个函数等同
function f(x){
  g(x);
}
function f(x){
  g(x);
  return undefined;
}

// 尾调用, 虽然尾调用没出现在函数尾部,但是只要是最后一步操作即可
function f(x) {
  if (x > 0) {
    return m(x)
  }
  return n(x);
}

尾调用优化

  • 函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到A,B的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)。
  • 尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。
  • 只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行“尾调用优化”。
// 不会进行尾调用优化,因为内层函数inner用到了外层函数addOne的内部变量one。
function addOne(a){
  var one = 1;
  function inner(b){
    return b + one;
  }
  return inner(a);
}

尾递归

  • 函数调用自身,称为递归。如果尾调用自身,就称为尾递归。
  • 递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。
// 正常递归,复杂度 O(n)
function factorial(n) {
  if (n === 1) return 1;
  return n * factorial(n - 1);
}

// 尾递归,复杂度 O(1)
function factorial(n, total = 1) {
  if (n === 1) return total;
  return factorial(n - 1, n * total);
}

来源: http://es6.ruanyifeng.com/#docs/function#尾调用优化

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容