一、尾调用
1、概念
在函数的最后一个调用其他函数,则是尾调用
function a(x) {
return b(x)
}
2、区分尾调用
// 这两种情况不属于尾调用
// return语句不能包含表达式
function a(x) {
return b(x) + 1
}
function a(x) {
let y = b(x)
return y
}
function a(x) {
b(x)
// return undefined
}
// 尾调用也不一定是在函数尾部,主要是函数最后一步操作就可
function a(x) {
if (x > 1) {
return m(x)
}
return n(x)
}
3、尾调用优化
函数调用会有一个“调用记录”,也称为“调用帧”
,用于保存调用位置和内部变量等信息。
如函数A里调用了函数B,则A的调用记录上会记录B的调用记录,等待B执行完毕后将结果给A,B才会消失。如果函数B调用了函数C,也会生成一个C的调用记录,以此类推,所有的记录就形成了“调用栈”
。
举个例子
function A() {
console.log(11)
}
function B() {
console.log(22)
A()
}
function C() {
console.log(33)
B()
}
C();
// 执行顺序 A() -> B() -> C() 33 22 11
使用尾调用优化实现
function A() {
console.log(11111)
}
function B() {
console.log(22)
return A()
}
function C() {
console.log(33)
return B()
}
C();
// 执行顺序 A() -> B() -> C()
执行函数C时,最后一步return函数B,所以函数C可以删除,因此调用栈中只有一个函数B,函数B执行完后调用函数A,也清除函数B的记录,以此类推,可以得出,尾调用由于是函数的最后一步操作,就无需保存记录,因此在调用栈中一直保留一个调用帧。
function f() {
let m = 1;
let n = 2;
return g(m + n);
}
f();
// 等同于
function f() {
return g(3);
}
f();
// 等同于
g(3);
上面代码中,如果函数g不是尾调用,函数f就需要保存内部变量m和n的值、g的调用位置等信息。但由于调用g之后,函数f就结束了,所以执行到最后一步,完全可以删除 f() 的调用记录,只保留 g(3) 的调用记录。
只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行“尾调用优化”。
这就叫做"尾调用优化"(Tail call optimization),即只保留内层函数的调用记录。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用记录只有一项,这将大大节省内存。这就是"尾调用优化"的意义。
注意,目前只有 Safari 浏览器支持尾调用优化,Chrome 和 Firefox 都不支持
二、尾递归
1、普通递归
概念:函数内部调用自身
解决的问题:
① 数据的定义是按递归进行的(阶乘)
② 问题的解法按递归实现(回溯)
③ 数据的结构按照递归定义的(树形结构、二叉树)
缺点:运行效率低、容易造成栈溢出
2、尾递归
概念:尾调用自身,就称为尾递归
递归很耗内存,每次调用都会保存所有的调用记录,容易发生栈溢出错误。但对于尾调用来说
每次都只存在一个调用记录,就不会发生栈溢出,相对节省内存,优化了性能。
(1)实现阶乘
递归实现:
function factorial(n) {
if (n === 1) return 1;
return n * factorial(n - 1);
}
factorial(5) // 120
调用形式:
factorial(5)
5 * factorial(4)
5 * (4 * factorial(3))
...
// 计算阶乘的时候,需要保存n个调用记录,复杂度为O(n)
// 执行多层时会出现栈溢出
尾调用递归实现:
function factorial(n, total) {
if (n === 1) return total;
return factorial(n-1, n*total);
}
factorial(5, 1) // 120
调用形式:
factorial(5, 1)
factorial(4, 5)
factorial(3, 20)
...
// 尾递归则保留一个记录,复杂度为O(1)
执行阶乘函数factorial(5, 1)时,当调用栈次数过多,调用堆栈会一直增长,直到达到限制:浏览器硬编码堆栈大小或内存耗尽。会发生超出最大调用堆栈大小“Maximum call stack size exceeded”
,
还可通过斐波那契数列,可以体现到尾递归的重要性
(2)斐波那契数列
普通递归:
function feibonaqi(n) {
if (n <= 1) return 1;
return feibonaqi(n-1) + feibonaqi(n-2);
}
feibonaqi(10) //89
feibonaqi(20) //报错
尾递归:
function feibonaqi(n, a1 = 1, a2 = 1) {
if (n <= 1) return a2;
return feibonaqi(n-1, a2, a1+a2);
}
feibonaqi(10) //89
feibonaqi(20) // 10946
3、函数递归的改写
尾递归的实现,需要改写递归函数,确保是函数最后一步调用自身。需要把用到的内部变量变成函数的参数。
上面例子中,阶乘函数factorial使用了一个中间变量total,在进行阶乘时将total作为函数的参数。在初始时,传入了5和1两个参数,可能第一次不太直观看懂
有两种方法可以解决
方法一:
在尾递归函数之外,在提供另一个函数
function tailFactorial(n, total) {
if (n === 1) return total;
return tailFactorial(n - 1, n * total);
}
function factorial(n) {
return tailFactorial(n, 1);
}
factorial(5) // 120
函数式编程有一个方法--“函数式柯里化”,将多参函数转成单参函数
尾递归之所以需要优化,原因是调用栈太多,造成溢出,那么只要减少调用栈,就不会溢出。
方法二:
使用ES6函数默认值
function tailFactorial(n, total = 1) {
if (n === 1) return total;
return tailFactorial(n - 1, n * total);
}
factorial(5) // 120
三、函数柯里化
函数柯里化就是把一个多参函数转成单参函数
// 柯里化前-普通相加函数
function sum(x, y) {
return x + y;
}
sum(2,3) // 5
// 柯里化
function sum(x) {
return function(y) {
return x + y;
}
}
// 箭头函数
const add = a => b => a + b;
sun(2)(3)
// const f = sum(1)
// f(2)
尾递归阶乘函数使用柯里化,将多参转成单参函数
function currying(fn, n) {
return function (m) {
return fn.call(this, m, n);// 绑定this
};
}
function tailFactorial(n, total) {
if (n === 1) return total;
return tailFactorial(n - 1, n * total);
}
const factorial = currying(tailFactorial, 1);
factorial(5) // 120
四、严格模式
ES6的尾调用优化只在严格模式下开启,正常模式是无效的。
这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。
arguments
:返回调用时函数的参数。func.caller
:返回调用当前函数的那个函数。
尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。