js递归优化

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 最终指向相同的堆地址,所以对象拷贝比较头痛。
常见的深拷贝方法:

  1. 递归遍历
  2. 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递归综合比较都不太实用。

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值