1. 递归
递归即 函数中调用自身 ,eg.
fucntion sum(n){
if(n === 1) return 1
return n + sum(n-1)
}
这个sum()
就是一个递归函数
2. 尾调用
即只有在最后一步是调用的递归函数才叫做尾调用,且只调用了递归函数的情况才叫做尾调用,可以看到上面的例子里是不算的,下面的例子才算作尾调用,eg.
function sum(n,result){
if(n === 1) return result
return sum(n-1,result + n)
}
这个操作有什么好处呢?在浏览器环境下,执行内存里只有一个调用栈,资源是非常有限的,在这种情况下,非尾调用的递归方式会占用大量的内存空间,甚至会导致栈溢出报错,比如一个非尾递归的50000次的函数输入值缩减的递归操作,eg.
function subtract (n) {
if (n === 0) return 1
return subtract(n - 2)
}
subtract(50000)
上面代码并非是一个没有终点的递归操作,但是在浏览器中还是爆栈了,尽管使用递归尾调用方法也无法解决这个爆栈的问题,但是这里拥有了这么一个思路去解决执行栈内存的节约问题,后面我会回来继续解决这个问题。
尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用记录,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用记录,取代外层函数的调用记录就可以了。
3. 尾递归
尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。
function sum(n,result){
if(n === 1) return result
return sum(n-1,result*n)
}
就像上述提供的例子,这个写法唯一的问题就是不太直观,一眼看上去很难看出这是要做一个递归相加的操作
这里有两种方式解决这个问题
一个是在尾递归函数外,包裹一层正常的函数:
function sum(n,result){
if(n === 1) return result + 1
return sum(n-1,result + n)
}
function add(n){
return sum(n,0)
}
add(10) // 55
是不是正常多了
函数编程有一个概念叫做函数柯里化,意思是将多参数的函数转换成单参数的形式,这里可以使用柯里化:
function currying(fn,n){
return function(m){
return fn.call(this,m,n)
}
}
function sum(n,result){
if(n === 1) return result + 1
return sum(n-1,result + n)
}
const add = currying(sum,0)
add(10) // 55
上述代码通过柯里化,将尾递归函数转化为了只接受一个参数的情况
另外 es6 的函数默认值也可以处理:
function sum(n,result=0){
if(n === 1) return result + 1
return sum(n-1,result + n)
}
sum(10) // 55
最后,我们再来看下上面的那个爆栈的问题:
function subtract (n) {
if (n === 0) return 1
return subtract(n - 2)
}
subtract(50000)
这里 我断点走了一下,惊喜的发现 ,还是爆栈了。。。。
然后我搜索了一下相关的资料,发现目前对这块的讨论也不是很明确,segmentFault和stackoverflow上都对这块有过讨论,大意就是js目前对尾调用tail call支持不好,在node6.5+的版本中使用--harmony-tailcalls关键字是可以运行的,但是很多环境都还是不行,所以,如果遇到这种递归导致爆栈的情况的话,我推荐使用循环的方式代替递归的操作来进行使用,当然具体环境具体分析,在这里我也是抱着学习的态度进行了一部分总结,具体的信息可以去参考资料中进行详细的查看。
参考资料:
- https://zh.wikipedia.org/wiki/%E5%B0%BE%E8%B0%83%E7%94%A8 尾调用(wiki)
- Tail Calls, Default Arguments, and Excessive Recycling in ES-6 by raganwald
- Proper Tail Calls from programming in Lua
- https://segmentfault.com/q/1010000008385443/a-1020000008385612 segmentFault相关讨论
- https://stackoverflow.com/questions/39617910/cant-enable-tail-call-optimization-in-node-v6-4-0)) stackoverflow相关讨论
- http://kangax.github.io/compat-table/es6/#firefox71 ecmascript支持表