😁 作者简介:一名大三的学生,致力学习前端开发技术
⭐️个人主页:夜宵饽饽的主页
❔ 系列专栏:前端js专栏
👐学习格言:成功不是终点,失败也并非末日,最重要的是继续前进的勇气
🔥前言:
这里是关于函数尾调调优的部分,这部分中的部分代码需要大家亲手去运行一下,才能更好的理解,欢迎大家的补充和纠正
7.7 尾调用优化
7.7.1 什么是尾调用
尾调用是函数时编程一个重要的概念,是指某个函数的最后一步 是调用另一个函数
function f(x){
return g(x)
}
❗️ 注意:尾调用不一定出现在函数尾部,只要最后一步操作即可
7.7.2 尾调用优化
我们知道,函数调用会在内存中形成一个 “调用记录” 又称 “调用帧 " ,保存调用位置和内部变量等信息,如果在函数A内部调用函数B,那么在A的调用帧上方还会形成一个B的调用帧,等到B运行结束,将结果返回到A,B的调用帧才会消失,所有调用帧就会形成一个”调用栈“
尾调用由于是函数最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置内部的变量等信息不会再用到了,直接用内层函数调用帧取代外层函数即可,这就叫做 ”尾部调用优化“ 即只保留内层函数的调用帧
如果函数都是尾调用,那么完全可以做到每次执行时调用帧只有一项,这将大大节省内存,这就是尾调用优化的意义
❗️ 注意:
只有不再使用外层函数的内部变量,内层函数的调用帧才会取代外层函数调用帧,否则无法”尾调用优化“
7.7.3 尾递归
函数调用自身称为递归,如果尾调用自身就称为尾递归
递归非常耗费内存,因为需要同时保存成百上千个调用帧,很容易发生栈溢出错误,但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生”栈溢出“ 错误
function factorial(n){
if(n === 1) return 1;
return n*factorial(n-1);
}
factorial(5) //120
上面的代码是一个阶乘函数,复杂度为O(n)
如果我们用尾递归来写,只保留一个记录,则复杂度 O(1)
function factorial(n,total){
if(n===1) return total
return facorial(n-1,n*total)
}
factorial(5,1)
还有一个比较著名的例子——计算Fibonacci 数列
//非尾递归的 Fibonacci
function Fibonacci(n){
if(n<=1) {return 1}
return Fibonacci(n-1) +Fibonacci(n-2)
}
console.log(Fibonacci(10))
console.log(Fibonacci(100))
console.log(Fibonacci(500))
//尾递归优化的Dibonacci数列
function Fibonacci2(n,ac1=1,ac2=1){
if(n <= 1) {return ac2}
return Fibonacci2(n-1,ac2,ac1+ac2);
}
console.log(Fibonacci2(100))//573147844013817200000
console.log(Fibonacci2(1000)) //7.0330367711422765e+208
由此可见 ”尾调用优化“ 对递归操作意义重大,所以一些函数式编程语言将其写入语言规格,ES6也是如此,第一次明确规定,所有ECMAScript的实现都必须部署 ”尾调用优化“ 这就是说,ES6中,只要使用尾递归,就不会发生栈溢出,相对节省内存
7.7.4 递归函数的改写
刚刚的尾调用的递归函数实现,使用一个中间变量total当成函数的参数,这样做的缺点是函数不太直观,易读
这个问题我们有两种方法解决
第一种:在尾递归函数之外,再提供一个正常形式的函数
function tailFactorial(n,total){
if(n===1) return total
return facorial(n-1,n*total)
}
function factorial(n){
return tailFactorial(n,1)
}
factorial(5)
第二种,采用函数的默认值
function factorial(n,total=1){
if(n===1) return total
return facorial(n-1,n*total)
}
factorial(5)
总结:递归的本质就是一种循环操作,纯粹的函数式编程语言是没有循环操作命令的,所有的循环都用递归实现,这就是为什么尾递归对这些语言的重要性,对于其他支持”尾调用优化“ 的语言,(比如Lua,ES6)只需要知道循环可以使用递归代替,而一旦使用递归,最好使用尾递归
7.7.5 严格模式
ES6的尾调优化只在严格模式下开启,正常模式下是无效的
这是因为,在正常模式下函数内部有两个变量,可以跟踪函数的调用栈。
- func.arguments :返回调用时函数的参数
- func.caller:返回调用当前函数的那个函数
7.7.6 尾递归优化的实现
尾递归优化只在严格模式下生效,那么正常模式下,或者在哪些不支持该功能的环境中,有没有办法使用尾递归优化呢》答案是肯定的——我们可以自己实现尾递归优化
思路:尾递归之所以要优化,原因是调用栈太多造成溢出,那么我们只要减少调用栈就不会溢出,我们可以采用 ”循环“ 替换 ”递归“
fcuntion sum(x,y){
if(y>0){
return sum (x+1,y-1)
}else{
return x
}
}
sum(1,100000)
// Uncaught RangerError:Maximum call stack size exceeded(...)
上面的代码中,sum是一个递归函数,参数x是需要累加的值,参数y控制递归次数,而选择100000次数,已经超出调用栈最大的次数了
我们可以使用蹦床函数将递归执行转为循环执行
function trampoline(f){
while (f && f instanceof Function){
f=f()
}
return f;
}
这个蹦床函数就是接受函数f作为参数,只要f执行后返回函数,那就继续执行
这里返回一个函数,然后执行该函数,而不是在函数里面调用函数,这样就可以避免了递归执行
我们可以选择这样做
fcuntion sum(x,y){
if(y>0){
return sum.bind(null,x+1,y-1)
}else{
return x
}
}
上面的代码中,sum函数每次执行时都会返回自身的另一个版本,因为bind方法会返回去执行一个新的函数
但是蹦床函数还不是真正的尾递归优化,下面的实现才是
function tco(f){
var value;
var active=false
var accumulated =[]
return function accumulator(){
accumulated.push(arguments);
// console.log(accumulated)
if(!active){
active=true
while(accumulated.length){
value=f.apply(this,accumulated.shift())
}
active=false
return value
}
}
}
var sum=tco(function(x,y){
if(y>0){
return sum(x+1,y-1)
}
else{
return x
}
})
console.log(sum(1,10))
**tco函数时尾递归优化的实现,它的奥妙在于状态变量action 默认情况下,这个变量不被激活,一旦进入尾递归优化的过程,这个变量激活,然后每一轮sum返回都是undefined 所以就避免了递归执行,而accumulated数组存放每一轮sum执行的参数,总是有值的,这就保证了其中的while循环总是会执行的,很巧妙的将 ” 递归“ 改成了 ”循环”,而后一轮参数会取代前一轮的参数,保证函数的调用栈只有一层 **
👨 我的思考:
- 在这个sum变量调用的匿名函数中,与原来的sum正常的递归函数的差别在,到底有没有依赖return x
- 经过debug发现,这个函数当想要继续在函数里面执行函数时,会返回undefined ,很好的避免了递归
- 其return value和while的作用就是让函数继续执行,达到累加的目的
7.8 函数参数的尾逗号
ES2017有一个提案,允许函数的最后一个参数有尾逗号,这样的规定也使得函数参数与数组和对象得尾逗号规则可以保持一致