赞
踩
尾调用(Tail Call)是函数式编程的一个重要概念,简单概括就是:某个函数的最后一步是调用另一个函数。
function f(x){
return g(x); // 函数f的最后一步是调用函数g,这就属于尾调用
}
// 尾调用不一定出现在函数尾部,只需要是最后一步即可
function f(x) {
if (x > 0) {
return m(x)
}
return n(x);
}
以下是几种不属于尾调用的函数
function f(x){
let y = g(x);
return y; // 调用函数g以后,还有赋值操作
}
function f(x){
return g(x) + 1; // 调用函数g以后,还有其他的操作
}
要想了解尾调用,必须先要了解函数的调用栈
函数调会在内存中形成一个“调用记录”(“调用帧”),保存调用位置和内部变量等信息。如果在函数a中调用函数b,则会在a函数的调用记录上面,形成一个b函数的调用记录。等到b函数运行结束,将结果返回a函数,b函数的调用记录才会消失。如果b函数中还调用了函数c,那就会形成一个函数c的调用栈。以此类推,所有的调用记录,形成了一个“调用记录”。
function a (){
return b()
}
很明显,尾调用优化之前,没嵌套一个内部函数,调用栈中便多了一个栈帧。尾调用优化之后,调用栈中至始至终都只有一个栈帧
函数调用自身,称为递归。如果尾调用自身,则称为尾递归。
递归通常非常消耗内存,因为要同时保存成百上千个调用记录,很容易发生“栈溢出”错误,但是对于尾递归来说,由于只存在一个调用记录,所以不会发生“栈溢出”的错误。
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}
factorial(5) // 120
上面是一个阶乘函数,计算n的阶乘,最多需要保存n个调用记录
如果改写成尾递归
function factorial(n, total) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
factorial(5, 1) // 120
使用尾递归以后,函数始终只由一个调用记录,不会造成“栈溢出”,相对节省内存。
尾递归的实现,往往需要改写递归函数,确保在最后一步调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。比如上面的例子,阶乘函数 factorial 需要用到一个中间变量 total ,那就把这个中间变量改写成函数的参数。这样做的缺点就是不太直观,第一眼很难看出来。
可以采用es6中的默认参数来优化这个问题
function factorial(n, total = 1) {
if (n === 1) return total;
return factorial(n - 1, n * total);
}
factorial(5) // 120
上面的代码中,默认参数有值,所以调用的时候不再提供这个值。
es6中的尾调用优化只有在严格模式才能生效,正常模式是无效的
因为在正常模式下,函数内部有2个参数可以跟踪函数的调用栈
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。