闭包
对于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设计模式与开发实践》