前言
前两天笔试的时候遇到了这个问题,看了多篇博文之后终于理解了,在这里记录一下~
什么是尾调用
简单来说,一个函数的返回值是对另外一个函数的调用,这种情况就叫做尾调用。
function f1(n) {
return n * 2
}
function f2(n) {
return f1(n + 1) //line A
}
const res=f2(2))//line B
在以上代码中,f2的返回值是对f1的调用,这就是尾调用
。
对尾调用的优化
首先我们要知道尾调优化是浏览器帮我们完成的工作,只要满足特定条件,这种优化就会被触发。
我们都知道:在代码执行时,会产生一个调用栈,调用某个函数时会将其压入栈,当它 return 后就会出栈。
以上面的代码为例我们对ES5和ES6的不同处理方案进行分析。
在ES5中:
- 调用函数f2,f2入栈,并记录下调用它的地方,以便知道返回值。
- 在尾部调用的函数f1会被推入新栈帧来表示调用,在入栈时记录下调用它的地方。当得到该函数的返回值时,f1执行结束,弹栈。
- f2得到了f1的返回值,f2的执行结束了,弹栈。
可以看出在这次调用中,每一个没有被用完的栈帧都保留在内存中。
观察可知,f1
的执行结果不必先到达lineA
,而是可以直接到达lineB
。为什么呢?因为f2的返回值就是f1的返回值,没有在此基础上做任何操作。
在ES6中,就针对这种情况做了优化。
在ES6中:
缩减严格模式里尾调用栈帧的大小,如果满足以下条件,不再创建新栈帧,而是清空并重用当前函数的栈帧:
- 尾调用不再访问当前栈帧的变量(函数不是闭包)
- 尾调用的语句在函数的最后一行
- 尾调用的结果
直接
作为函数的返回值
如何理解直接
呢?例如下面的情况就不会做尾调优化
function f1(n) {
return n * 2
}
function f2(n) {
//在f2中并不是直接返回f1的结果,而是要进行加一的操作
return 1+f1(n + 1) //line A
}
const res=f2(2))//line B
原因也很好理解,因为f1的返回值不能直接到达lineB
,而是要先到达lineA
进行加1。
尾递归
什么是尾递归呢?其实就是尾调用的特殊形式:当尾调用是对自身的调用时,就是尾递归。
const sum = (n) => {
if (n <= 1) return n;
return n + sum(n-1)
}
sum(5)
这段代码的目的是用来计算1到5的和,但是很显然这种情况并不会做尾调优化,因为尾调函数不是直接return
,而是要先加n
。当n的数字增大的时候,因为之前的调用栈不能被释放掉,会造成栈溢出的问题。
我们可以利用ES6的尾调优化特性对上面的代码进行改写:
const sum = (n, preSum = 0) => {
if (n <= 1) return n + preSum
else {
// 现在的sum是preSum+n,递归算1~n-1的sum
return sum(n - 1, preSum + n)
}
}
sum(5)
这样理论上来说没有了最大栈限制的问题,但是我在实际测试(用chrome)是发现还是会有栈溢出的问题,查了之后说是有些浏览器还没支持这种优化。fine~TAT