1、尾调用优化
尾调用,简单的说,就是一个函数执行的最后一步是将另外一个函数调用并返回。
以下是正确示范:
function foo(n){
return bar(n);
}
function func(x){
if(x > 0){
return bar(x);
}
return bar(x);
}
以下是错误示范:
function foo(x){
var result = bar(x);
return result;
}
function bar(y){
return g(y) + 1;
}
function func(z){
t();
}
在调用栈的部分我们知道,当一个函数A调用另外一个函数B时,就会形成栈帧,在调用栈内同时存在调用帧A和当前帧B,这是因为当函数B执行完成后,还需要将执行权返回A,那么函数A内部的变量,调用函数B的位置等信息都必须保存在调用帧A中。不然,当函数B执行完继续执行函数A时,就会乱套。如果递归调用就会形成很多调用帧,最后导致调用栈溢出。
那么现在,我们将函数B放到了函数A的最后一步调用(即尾调用),那还有必要保留函数A的栈帧么?当然不用,因为之后并不会再用到其调用位置、内部变量。因此直接释放调用帧A即可。当然,如果内层函数使用了外层函数的变量,那么就仍然需要保留函数A的栈帧,典型例子即是闭包。
典型案例,Fibonacci数列
/*
* 常规写法
*/
function fibonacci(n){
if(n <= 1){
return 1;
}
return fibonacci(n - 1) + fibonacci(n - 2);
}
console.time('Recursive call');
console.log(fibonacci(40));
console.timeEnd('Recursive call');
才第40个,计算就已经非常慢,第100个的时候就栈溢出了
/*
* 尾调用写法
*/
function fibonacciTail(n,ac1 = 1,ac2 = 1){
if(n <= 1){
return ac2;
}
return fibonacciTail(n - 1,ac2,ac1 + ac2);
}
console.time('tail call');
console.log(fibonacciTail(15000));
console.timeEnd('tail call');
效果截图,第15000个的时候,依然很快,到20000个的时候,依旧栈溢出
/*
* 循环写法
*/
function fibonacciLoop(n){
var num1 = num2 = 1;
while(--n){
[num1,num2] = [num2,num1 + num2];
}
return num2;
}
console.time('loop call');
console.log(fibonacciLoop(100000));
console.timeEnd('loop call');
效果截图,循环写法到10万个依旧没有栈溢出,效率也很高
总体来说:
写法 | 优点 | 缺点 |
---|---|---|
递归写法 | 简单明了 | 性能太差,容易栈溢出 |
尾调用写法 | 性能较好,且也直观 | 中规中矩,递归多还是会栈溢出,es6严格模式下才有效 |
循环写法 | 性能强 ,理论上不会栈溢出 | 不直观 |
2、尾调用替换方案,蹦床函数
尾递归优化只在严格模式下生效,那么正常模式下,或者那些不支持该功能的环境中,蹦床函数也是一种解决方案,原理就是自行实现尾调用
/*
* 蹦床函数尾调用写法
*/
function trampoline(func){
while(func && func instanceof Function){
func = func();
}
return func;
}
function fibonacciTrampoline(n,ac1,ac2){
ac1 = ac1 === undefined ? 1 : ac1;
ac2 = ac2 === undefined ? 1 : ac2;
if(n <= 1){
return ac2;
}
return fibonacciTrampoline.bind(null,n - 1,ac2,ac1 + ac2);
}
console.time('trampoline call');
console.log(trampoline(fibonacciTrampoline(10000)));
console.timeEnd('trampoline call');
利用bind方法返回函数,循环调用改方法。因为原理是循环,所里原则上也不会出现栈溢出。
3、附加实例–对象深拷贝
在JavaScript中,变量分为基本类型和应用类型,基本类型存储在栈中,引用类型存储在堆中,Obejct类型基本都是引用类型,在赋值的时候,实则是变量名的拷贝,obj1 = obj2 最终指向相同的堆地址,所以对象拷贝比较头痛。
常见的深拷贝方法:
- 递归遍历
- JSON拷贝
下面的代码可以生成指定深度和每层广度的代码下面的代码可以生成指定深度和每层广度的代码
/*
* 创建对象
* @params {deep},Number;深度
* @params {deep},Number;广度
* @return; new object
*/
function createObject(deep,breadth){
var result = tmp = {};
for(var i = 0; i < deep; i++){//object deep
tmp = tmp['data'] = {};
for(var j = 0; j < breadth; j++){//object breadth
tmp[j] = j;
}
}
return result;
}
1、递归深拷贝
/*
* 深拷贝 Object
* @params {obj},Object;
* return; a new object
*/
function clone(obj){
if(Object.prototype.toString.call(obj) !== '[object Object]'){
return obj;
}
var result = {};
for(var key in obj){
if(obj.hasOwnProperty(key)){
var val = obj[key];
if(Object.prototype.toString.call(obj) === '[object Object]'){
result[key] = clone(val);
}else{
result[key] = val;
}
}
}
return result;
}
console.time('clone10000');
console.log(clone(createObject(10000)));
console.timeEnd('clone10000');
console.time('clone20000');
console.log(clone(createObject(20000)));
console.timeEnd('clone20000');
当深度在10000的时候ok,20000的时候就爆栈了
2、JSON拷贝
console.time('jsonClone5000');
console.log(JSON.parse(JSON.stringify((createObject(5000)))));
console.timeEnd('jsonClone5000');
console.time('jsonClone10000');
console.log(JSON.parse(JSON.stringify((createObject(10000)))));
console.timeEnd('jsonClone10000');
JSON拷贝深拷贝比较方便,但是功能缺陷比较多,首先拷贝的对象中,例如Function,undefined之类的对象会失效,然后对象深度10000就爆栈了
而且,以上方法拷贝对象循环引用就会爆栈
var a = {};
a.a = a;
console.log(clone(a));
console.log(JSON.parse(JSON.stringify(a)));
下面是我们改写clone递归方法为循环方法
/*
* 纯对象深拷贝
* @params{obj}, Object;拷贝对象
* @params{obj}, Boolean;true: 拷贝所有,false: 相同项不拷贝
* example
var a = {a: 'a'},
obj = {a: a,b: 'b',c: a};
cloneObj = objectDeepClone(obj,true);
cloneObj.a.a = 'changed';
console.log(cloneObj.c.a);//a
cloneObj = objectDeepClone(obj,false);
cloneObj.a.a = 'changed';
console.log(cloneObj.c.a);//changed
*/
function objectDeepClone(obj,isForce){
if(!obj || Object.prototype.toString.call(obj) !== '[object Object]'){
return obj;
}
var uniqueList = [];
isForce = !!isForce;
var result = {},
loopList = [{
parent: result,
key: undefined,
data: obj
}];
while(loopList.length){
var node = loopList.pop(),
parent = node.parent,
key = node.key,
data = node.data,
tempRes = parent;
if(key !== undefined){
tempRes = parent[key] = {};
}
if(!isForce){
var uniqueData = uniqueFind(uniqueList,data);
if(uniqueData){
parent[key] = uniqueData.target;
continue;
}
}
uniqueList.push({
source: data,
target: tempRes
});
for(var k in data){
if(data.hasOwnProperty(k)){
var val = data[k];
if(Object.prototype.toString.call(val) === '[object Object]'){
loopList.push({
parent: tempRes,
key: k,
data: val
});
}else{
tempRes[k] = val;
}
}
}
}
uniqueList = null;
return result;
}
/*
* 数组中循环查找存在对象
* @params{arr} Array;目标对象
* @params{item} any type;查找对象
* @return 查找结果,没有返回null
*/
function uniqueFind(arr,item){
var len = arr.length;
while(len--){
if(arr[len].source === item){
return arr[len];
}
}
return null;
}
借助数组改用循环后,再也不会出现爆栈的问题了,循环引用问题也迎刃而解,如果需要拷贝其他引用类型,则需稍稍改写即可。
总体来说,能不用递归,尽量不用,要用也推荐用尾递归或改写的蹦床函数。将递归改写成循环就不会出现爆栈的情况,性能也较其他方法高。这也是一种思想吧,毕竟js递归综合比较都不太实用。