18-2 尾调用/尾递归/尾递归优化/柯里化函数/蹦床函数
1.尾调用:就是指某个函数的最后一步是调用另一个函数的,并且调用的这个函数必须被return出去的
尾调用不一定出现在函数尾部,只要是最后一步操作即可。
情况一:
function func1(x) {
let y = g(x); //g函数调用后还有赋值的操作,不符合
return y;
}
// 情况二
function func2(x) {
return g(x) + 1; //g执行也进行了+1操作,不符合
}
// 情况三
function func3(x) {
g(x); // 返回了unfined,不符合
}
2.尾调用优化:
尾调用之所以与其他调用不同,就在于它的特殊的调用位置
我们知道,函数调用时候会在内存形成一个“调用记录”,又称“调用帧”,保存调用位置和内部变量等信息。
如果函数A的内部调用函数B,那么在A的调用调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到A,
B的调用帧才会消失。如果B的内部还有函数C,那就还有一个C的调用帧,以次类推,所有的调用帧就形成了“调用栈”。
尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧了,因为调用位置,内部变量等信息都不会再用到了,
只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。
function fun() {
let m = 1;
let n = 2;
return g(m + n);
}
fun();
// 等同于
function fun2() {
return g(3);
}
fun2();
// 等同于
g(3);
3.尾递归 : 函数调用自身成为递归。如果尾调用自身,就称为尾递归。
// 复杂度O/n
function fn(n) {
if (!n) return 1;
return fn(n - 1);
}
let res = fn(1000);
console.log(res); // 1
尾递归改写:
尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。
如下面这个例子:
// 复杂度 O/1
function fn1(n, m) {
if (!n) return m;
return fn1(n - 1, m);
}
let res1 = fn1(100, 1); //如果传入fn1(1000000,1) 输出:Maximum call stack size exceeded,原因分析请看下面[蹦床函数的由来:是解决什么问题的?]
console.log(res1); // 1
所以问题就又来了,上面虽然做了把内部变量变为了函数的参数,但是这样的缺点就是不太直观,第一眼很难看出来,凭什么要多传一个1?
于是我们有两个方式可以解决这个问题:
(1)第一种:用一个中间函数包装一下,执行这个中间函数传入一个值
function fn1(n, m) {
if (!n) return m;
return fn1(n - 1, m);
}
function tempFn(n) {
return fn1(n, 1)
}
tempFn(100)
(2)第二种:运用4.函数的柯里化思想
function curryFn(fn) {
let outerArgs = Array.prototype.slice.call(arguments, 1)
return (...args) => {
return fn.call(this, ...args, ...outerArgs)
}
}
function fn1(n, m) {
if (!n) return m;
return fn1(n - 1, m);
}
var cFn = curryFn(fn1, 1) //这个也可以说是偏函数,把固定要传的参数预置好
var res = cFn(100)
console.log(res); // 1
第三种:通过函数参数的默认值
function fn1(n, m = 1) {
if (!n) return m;
return fn1(n - 1, m)
}
let res = fn1(100)
console.log(res); //1
注意:
ES5中不存在尾递归,并且ES6的尾调用优化只在严格模式下开启,正常模式是无效的。
- 这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈
func.arguments:返回调用时函数的参数。
func.caller:返回调用当前函数的那个函数。
- 尾调用优化发生时,函数的调用栈会改写,因此上面这两个变量就会失真。严格模式禁用了这俩变量,所以尾调用模式只在严格模式下生效
function restricted() {
"use strict";
restricted.caller; // 报错
restricted.arguments; // 报错
}
restricted();
既然尾递归优化只在严格模式下生效,那么正常模式下,或者那些不支持该功能的环境中,我们要想使用递归尾优化,需要自己实现尾递归优化:
那我们想了,尾递归之所以需要优化,原因无非就是调用栈太多,造成溢出,那么只要减少调用栈,就不会溢出。怎么做可以减少调用栈呢?
就是采用“循环”替换掉“递归”
// 先看一个正常递归函数 function sum(x, y) { if (y > 0) { return sum(x + 1, y - 1) } else { return x } } let r = sum(1, 10000) console.log(r); // Maximum call stack size exceeded
// ----------------------------------------------------------------------------------------------------------------------- // 蹦床函数可以将递归执行转为循环执行:本质就是将原来的递归函数,通过fn.bind()改写为每一步返回另一个函数 function trampoline(f) { while (f && f instanceof Function) { f = f() } return f } // 通过bind返回一个新函数,而不是在sum中又调了sum自身,这样就避免了递归执行。从而就消除调用栈过大的问题 function sum(x, y) { if (y > 0) { return sum.bind(null, x + 1, y - 1) } else { return x } } var result = trampoline(sum(1, 10000)) console.log(result); //就不会发生栈溢出,输出:10001
5.蹦床函数的由来:是解决什么问题的?
// 看下面的例子:
function testFn(n) {
if (!n) return n;
return testFn(n - 1)
}
var res = testFn(123456)
console.log(res); // Maximum call stack size exceeded
分析:为什么会报"Maximum call stack size exceeded"的错误?
- 我觉得原因是在每次递归的时候,会把当前作用域里面的基本类型的值推进栈中,所以一旦递归层数过多,桟就会溢出,所以会报错
注意:
1. js中的桟只会存储基本类型的值:number,string,undefined,null,boolean
2. 为什么在调用下一层递归函数的时候没有释放上一层递归函数的作用域?因为在回来的时候还需要用到里面的变量。
// 改写:
function testFn(n) {
if (!n) return n;
return function () {
return testFn(n - 1)
}
}
var res = testFn(2)()()
console.log(res); // 0
分析:这个改写就是建立一个闭包来封装递归函数,他的好处是由于不是直接的调用,所以不会把上一次的递归作用域
推进栈中,而是把封装函数存储在堆内存中,利用这个容量更大但读取时间更慢的存储形式来替代栈这个容量小
但读取速度快的存储形式,用时间来换取空间。
结论: 通过上面的改写示例,如果参数不是2(需要执行2次,才能走到if语句中),我们就需要写多个括号来调用很多次。
如果testFn(12345678) 你要想执行到最后的!n-> n为0,那么后面需要执行12345678次 testFn(12345678)()()()....() 12345678次
为了简便,我们可以把这种调用方式,写成函数 ->蹦床函数:
function bengchuangFn(f) {
while (f && f instanceof Function) {
f = f()
}
return f
}
function testFn(n) {
if (!n) return n;
return function () {
return testFn(n - 1)
}
}
console.log(bengchuangFn(testFn(12345678))); // 由于存储在堆中,所以耗时较长,过了一会才会输出 0,但是并不会报桟内存溢出的错误。
另外一种写法:
function bengchuangFn2(f, num) {
let result = f.call(f, num);
while (result && typeof result === 'function') {
result = result()
}
return result
}
function testFn(n) {
if (!n) return n;
return function () {
return testFn(n - 1)
}
}
bengchuangFn2(testFn, 123456) //过一会输出0
平铺数组:
var arr = [1, [2, [3, 4, [5, 6, [7, [8, [9]]]]]]];
function bengchuang(f) {
while (f && f instanceof Function) {
f = f();
}
return f;
}
function run(arr) {
let result = [];
function inner(arr) {
arr.forEach((item) => {
if (Array.isArray(item)) {
return bengchuang(() => {
inner(item);
});
}
result.push(item);
});
}
inner(arr);
return result;
}
console.log(run(arr));