4

title: 被你忽略的‘尾调用’
date: 2017-05-02 16:52:22

tags: [ES6,javascript]

尾调用是什么?

在ES6有一个新特性:尾调用
用最简单的一句话描述就是‘某个函数的最后一步再调用另一个函数’,听起来挺简单的,
但是它的功能特别强大,直接给你撸个例子吧。

function a(x) {
  return b(x);
}

这个函数的最后一步再调用另一个函数,这就是尾调用。
以下几点不属于尾调用

  1. 在调用函数b之后还存在赋值的操作

function a(x) {
    let m = b(x);
    return m;
}
  1. 返回的那个函数没有加return

function a(x) {
    b(x);
}
  1. 在调用之后还存在其他的赋值操作

function a(x) {
    return b(x) - 2;
}

尾调用优化

函数调用会在内存形成一个"调用记录",又称"调用帧"(call frame),保存调用位置
和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用记录上方,还会形成
一个B的调用记录。等到B运行结束,将结果返回到A,B的调用记录才会消失。如果函数B
内部还调用函数C,那就还有一个C的调用记录栈,以此类推。所有的调用记录,就形成一个
"调用栈"

来个例子吧

function a() {
  let p = 2;
  let q = 3;
  return b(p + q)
}
a();

仔细观察上面的代码就会发现啊a函数似乎是多余的吧,因为b函数是尾调用函数,执行到
这里函数a早就结束了,完全可以删除a函数了只保留b的调用帧即可。
尾调用优化:就是只保留内层函数的调用帧,如果所有函数都是尾调用,那么完全可以
做到每次执行时调用帧只能有一项,将大大节省内存,也就是尾调用的意义所在了。
注意的一点就是内层函数运用了外层函数的变量便不能进行尾调用优化了。记住哦!

尾递归

顾名思义,在一个尾调用中,如果函数最后的尾调用位置上是这个函数本身,则被称为尾递
归。递归很常用,但如果没写好的话也会非常消耗内存,导致爆栈。但是对于尾递归来说只
存在一个调用栈,便永远不会发生“栈溢出”错误。
就以求一个给出数的阶乘来探索吧。在传统的做法中利用n的递减乘上原函数,这样复杂度
便会很高,数据量一大便会发生“栈溢出”的错误了。

传统的解决办法

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

尾调用

function a(n, t) {
  if(n === 1) {
    return t;
  }
  return a(n - 1, n * t)
}

对比两个解决办法的复杂度就知道,尾调用只保留了一个记录。


keephhh
84 声望9 粉丝