递归
递归的概念大家应该都知道吧,平时开发中也经常用到,比如在做后台管理系统时路由菜单,就需要递归遍历嵌套的路由菜单。最经典的还是阶乘或累加函数,如:
function add(n){
if(n <= 0) return 0
return add(n-1) + n
}
add(4) // 10
总结一下有两个特点:
- 函数自己调用自己
- 要有临界出门条件,不能无限制的调用
尾调用
直接举一些例子看注释吧
// 不是尾调用,函数的最后一步虽然是一个函数调用,但是并没有return
function fn(n){
gn(n)
}
// 不是尾调用,函数执行到最后一步,要等gn执行完毕得到结果后再计算才返回结果
function fn(n){
return gn(n) + 1
}
// 不是尾调用,因为在最后是个赋值操作,所以会先等gn执行完毕后fn才执行
function fn(n){
var res = gn(n)
return res
}
// 尾调用,函数最后一步返回一个函数
function fn(n){
return gn(n)
}
// 尾递归,函数最后尾调用自身
function fn(n){
if(n === 0) return 0
return fn(n-1)
}
总结一下
- 函数内部的最后一步要返回一个函数调用
- 函数尾调用自身则成为尾递归
区别
看起来递归和尾递归长的都差不多,可实际上差别可大了,特别是性能上。
其实就是执行上下文栈变化不一样,我们来模拟一下,看看下面这两个例子的区别,我们定义执行上下栈是一个数组
stack = [] //执行栈
//尾调用
function fn(x){
return gn(x)
}
//非尾调用
function fn(x){
return gn(x) + 1
}
首先模拟一下尾调用:
// 伪代码
stack.push(<fn> functionContext)
stack.pop()
stack.push(<gn> functionContext)
stack.pop()
再模拟一下非尾调用
//伪代码
stack.push(<fn> functionContext)
stack.push(<gn> functionContext)
stack.pop()
stack.pop()
根据伪代码可以很容易的看到最大的区别就是stack的长度变化了。
我们每次调用函数时,都会把其推入执行栈中,执行完毕才会释放,在上面的***非尾调***用函数中最后一句是gn(x) + 1
,这是需要等到gn函数执行完得出结果后再运算后才算fn函数执行完毕,即执行栈中fn
还没释放又推入了gn
。 而在***尾调用***中,最后是return gn,表示fn已经执行完毕出栈了,所以执行栈内永远只有一个函数。如果在大量的递归中,可能就会纯正堆栈溢出的情况了,而用尾递归就不会
用尾递归优化代码
回到最开始的累加函数,我们使用尾递归来优化一下
function add(n, sum){
if(n <= 0) return sum
return add(n-1, n + sum)
}
add(4,0) // 10
每次递归调用的时候,把结果当做参数传给下一次调用。
到这里就可以结束了,
然而…对于有小小强迫症的我来说,为什么调用add的时候要多传一个0,这有点难受~~,有几种方法美化一下
-
使用工厂函数
let newAdd = function(n) { return add(n,0) } newAdd(4)
-
使用es6的默认参数
function add(n, sum = 0){ if(n <= 0) return sum return add(n-1, n + sum) } add(4) // 10
-
使用柯里化
let curry = (fn, sum) => n => fn(n, sum)
let newAdd = curry(add, 0)
newAdd(4) // 10
over~~