闭包的理解

闭包

对于JavaScript程序员来说,闭包(closure)是一个难懂又必须政府的概念。闭包的形式与变量的作用域以及变量的生命周期密切相关。闭包在实际开发中运用非常广泛,简单几个例子对闭包进一步了解。

封装变量

闭包可以帮助把一些不需要暴露在全局的变量封装成“私有变量”。假设有一个计算乘积的简单函数:

var mult = function(){
	var a = 1;
	for (var i = =, l = arguments.length; i < 1; i++){
		a - a * arguments[i];
	};
	return a;
};

mult函数接受一些number类型的参数,并返回这些参数的乘积。现在我们觉得对于那些相同的参数来书,每次都进行计算是一种浪费,我们可以加入缓存机制来提高这个函数的性能:

var cache = {};

var mult = function(){
	var args = Array.prototype.join.call(arguments, ',');
	if (cache[ args ]){
		return cache[ args ];
	};
	var a = 1;
	for (var i = 0, l = arguments.length; i < l; i++){
		a = a * arguments[i];
	};
	return cache[ args ] = a;
};

alert( nult(1, 2, 3) ); // 输出6
alert( nult(1, 2, 3) ); // 输出6

我们看到cache这个变量仅仅在mult函数被使用,与其让cache变量跟mult函数一起平行地暴露在全局作用于下,不如把它封闭在mult函数内部,这样可以减少页面中的全局变量,以避免这个变量在其他地方不小心修改而引发错误。

var mult = (function(){
	var cache = {};
	return function(){
		var args = Array.prototype.join.call( arguments, ',');
		if ( args in cache ) {
			return cache[ args ];
		}
		var a = 1;
		for (var i = 0, l = arguments.length; i < l; i++ ){
			a = a * arguments[i];
		}
		return cache[ args ] = a;
	};
})();

提炼函数是代码重构中一种常见技巧。如果在一个大函数中有一些代码块能够独立出来,我们常常把这些代码块封装在独立的小函数里面。独立出来的小函数有助于代码复用,如果这些小函数有个良好的命名,他们本身也起到了注释的作用。如果这些小函数不需要在程序的其他地方使用,最好是把它们用闭包封闭起来。

var mult = (function(){
	var cache = {};
	var calculate = function(){ // 封闭calculate函数
		var a = 1;
		for ( var i = 0, l = arguments.length; i < l; i++ ){
			a = a * arguments[i];
		}
		return a;
	}

	return function() {
		var args = Array.prototype.join.call( arguments, ',' );
		if ( args in cache ) {
			return cache[ args ];
		}
		retuern cache[ args ] = calculate.apply( null, arguments );
	}
})();

延续局部变量的寿命

img对象经常用于数据上报,

var report = function( src ){
	var img = new Image();
	img.src = src;
};

report('http://xxx.com/getUserInfo');

但通过查询后台的记录我们得知,因为一些低版本浏览器的实现存在bug,在这些浏览器下使用report函数进行数据上报会丢失30%左右的数据,也就是说,report函数并不是每一次都成功发起HTTP请求。丢失数据的原因是img是report函数中的局部变量,当report函数的调用结束后,img局部变量随机被销毁,而此时或许还没来得及发处HTTP请求,所有此次请求就会丢失。现在我们把img变量用闭包封装起来,便能解决请求丢失的问题。

var report = (function(){
	var imgs = [];
	return function( src ){
		var img = new Image();
		imgs.push( img );
		img.src = src;
	}
})();

闭包和面向对象设计

过程与数据的结合是形容面向对象中的“对象”时经常使用的表达。对象以方法的形式包含了过程,而闭包则是在过程中以环境的形式包含了数据。通常用面向对象思维能实现的功能,用闭包也能实现。反之亦然。在Javascript语言的祖先Scheme语言中,甚至都没有提供面向对象的原生设计,但可以使用闭包来实现一个完整的面向对象系统。

var extent = function(){
	var value = 0;
	return {
		call: function(){
			value++;
			console.log( value );
		}
	}
};

var extent - extent();

extent.call(); // 输出1
extent.call(); // 输出2
extent.call(); // 输出3

// 如果换成面向对象的写法,就是
var extent = {
	value: 0,
	call: function(){
		this.value++;
		console.log( this.value );
	}
};

extent.call(); // 输出1
extent.call(); // 输出2
extent.call(); // 输出3

// 或者
var Extent = function(){
	this.value = 0;
};
Extent.prototype.call = function(){
	this.value++;
	console.log( this.value );
};

var extent new Extent()extent.call();
extent.call();
extent.call();

用闭包实现命令模式

在Javascript版本的各种设计模式实现中,闭包的运用非常广泛。

<html>
	<body>
	  <button id="execute">点击我执行命令</button>
	  <button id="undo">点击我执行命令</button>

	<script>
		var Tv = {
			open: function(){
				console.log('打开电视机');
			},
			close: function(){
				console.log('关闭电视机');
			}
		};

		var OpenTvCommand = function( receive ){
			this.receive = receive;
		};
		OpenTvCommand.prototype.execute = function(){
			this.receive.open(); // 执行命令,打开电视
		};
		OpenTvCommand.prototype.undo = function(){
			this.receive.close(); // 撤销命令,关闭电视
		};
		var setCommand = function( command ){
			document.getElementById('execute').onclick = function(){
				command.execute(); // 输出 打开电视
			}
			document.getElementById('undo').onclick = function(){
				command.undo(); // 输出 关闭电视
			}
		};
		setCommand( new OpenTvCommand( Tv ) );
	</script>
	</body>
</html>

命令模式的意图是把请求封装为对象,从而分离请求的发起者和请求的接收者(执行者)之间的耦合关系。在命令执行之前,可以预先往命令对象中植入命令的接收者。在JavaScript中,函数作为一等对象,本身就可以四处传递,用函数对象而不是普通对象来封装请求显得更加简单和自然。如果需要往函数对象中预先植入命令的接收者,那么闭包可以完成这个工作。在面向对象版本的命令模式中,预先植入的命令接收者被当成对象的属性保存起来;而在闭包版本的命令模式中,命令接收者会被封装在闭包形成的环境中。

var Tv = {
	open: function(){
		console.log('打开电视');
	},
	close: function(){
		console.log('关闭电视');
	}
};

var createCommand = function(){
	var execute = function(){
		return receiver.open(); // 执行命令,打开电视
	}
	var undo = function(){
		return receiver.close(); // 执行命令, 关闭电视
	}
	return {
		execute: execute,
		undo: undo
	}
};

var setCommand = function( command ){
	document.getElementByIb('execute').onclick = function(){
		command.execute(); // 输出,打开电视
	}
	documment.getElementById('undo').onclick = function(){
		command.undo(); // 输出,关闭电视
	}
};

setCommand( createCommand( Tv ) );

闭包与内存管理

闭包是一个非常强大的特性,但人们对其也有诸多误解。一种耸人听闻的说法是闭包会造成内存泄漏,所以要尽量减少闭包的使用。

局部变量本应该在函数退出的时候被解除引用,但如果局部变量被封闭在闭包形成的环境中,那么这个局部变量就能一直存在下去。从这个意义上看,闭包的确会使一些数据无法被及时销毁。使用必报的一部分原因是我们选择主动把一些变量封闭在闭包中,因为可能在以后还需要使用这些变量,把这些变量放在闭包中和放在全局作用域,对内存方面的影响是一致的,这里并不能说成内存泄漏。如果将来需要回收这些变量,可以手动把这些变量设置为null。

跟闭包和内存泄露有关系的地方是,使用闭包的同时比较容易形成循环引用,如果闭包的作用域链中保存着一些DOM节点,这时候就有可能造成内存泄漏。但这本身并非闭包的问题,也并非JavaScript的问题。在IE浏览器中,由于BOM和DOM中的对象是使用C++以COM对象的方式实现的,而COM对象的垃圾收集机制采用的是技术策略。在基于引用策略的垃圾回收机制中,如果两个对象之间形成了循环引用,那么这两个对象都无法被回收,但循环引用造成的内存泄漏在本质上也不是闭包造成的。

同样,如果要解决循环引用带来的内存泄漏问题,我们只需要把循环引用中的变量设置为null即可。将变量设置为nuil意味着切断变量与他此前引用的值之间的连接。当垃圾收集器下次运行时,就会删除这些值并回收它们占用的内存。

摘录自《JavaScript设计模式与开发实践》

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值