在 JavaScript 版本的设计模式中,许多模式都可以用闭包和高阶函数来实现。
闭包
闭包的形成与变量的作用域以及变量的生存周期密切相关。
- 变量的作用域
在 JavaScript 中,函数可以用来创造函数作用域。此时的函数像一层半透明的玻璃,在函数里面可以看到外面的变量,而在函数外面则无法看到函数里面的变量。这是因为当在函数中搜索一个变量的时候,如果该函数内并没有声明这个变量,那么此次搜索的过程会随着代码执行环境创建的作用域链往外层逐层搜索,一直搜索到全局对象为止,变量的搜索是从内到外而非从外到内的。
下面这段包含了嵌套函数的代码,也许能帮助我们加深对变量搜索过程的理解:
var a = 1;
var func1 = function(){
var b = 2;
var func2 = function(){
var c = 3;
alert ( b ); // 输出2
alert ( a ); // 输出:1
}
func2();
alert ( c ); // 输出:Uncaught ReferenceError: c is not defined
}
func1();
- 变量的生存周期
对于全局变量来说,全局变量的生存周期当然是永久的,除非我们主动销毁这个全局变量。
而对于在函数内用 var 关键字声明的局部变量来说,当退出函数时,这些局部变量即失去了它们的价值,它们都会随着函数调用的结束而被销毁。
现在来看看下面这段代码:
var func = function(){
var a = 1;
return function(){
a++;
alert ( a );
}
};
var f = func();
f(); // 输出:2
f(); // 输出:3
f(); // 输出:4
f(); // 输出:5
跟我们之前的推论相反,当退出函数后,局部变量 a 并没有消失,而是似乎一直在某个地方存活着。这是因为当执行 var f = func();时,f 返回了一个匿名函数的引用,它可以访问到 func()被调用时产生的环境,而局部变量 a 一直处在这个环境里。既然局部变量所在的环境还能被外界访问,这个局部变量就有了不被销毁的理由。在这里产生了一个闭包结构,局部变量的生命看起
来被延续了。
利用闭包我们可以完成许多奇妙的工作,下面介绍一个闭包的经典应用。
<html>
<body>
<div>1</div>
<div>2</div>
<div>3</div>
<div>4</div>
<div>5</div>
<script>
var nodes = document.getElementsByTagName( 'div' );
for ( var i = 0, len = nodes.len = nodes.length; i < len; i++ ){
nodes[ i ].onclick = function(){
alert ( i );
}
};
</script>
</body>
</html>
闭包实现代码:
for ( var i = 0, len = nodes.length; i < len; i++ ){
(function( i ){
nodes[ i ].onclick = function(){
console.log(i);
}
})( i )
};
根据同样的道理,我们还可以编写如下一段代码:
这段代码涉及的知识点:
1.for 循环写法
普通写法:
var type = ['String', 'Array', 'Number'];
for(var i = 0;i < type.length;i++) {
代码块
}
高级写法:赋值和判断写一起 (for循环第二个分号为判断条件 返回true则继续执行, 0, null, undefined, false, ‘’, “” 转化为false )
for (var i = 0, type;type = ['String', 'Array', 'Number'][i++]) {
代码块
}
2.Object.prototype.toString()
在JavaScript中,想要判断某个对象值属于哪种内置类型,最靠谱的做法就是通过Object.prototype.toString方法.
在toString方法被调用时,会执行下面的操作步骤:
1. 获取this对象的[[Class]]属性的值.
2. 计算出三个字符串"[object ", 第一步的操作结果Result(1), 以及 "]"连接后的新字符串.
3. 返回第二步的操作结果Result(2).
[[Class]]是一个内部属性,所有的对象(原生对象和宿主对象)都拥有该属性
原生对象的[[class]]内部属性的值一共有10种.分别是:"Array", "Boolean", "Date", "Error", "Function", "Math", "Number", "Object", "RegExp", "String".
var arr=[];
console.log(Object.prototype.toString.call(arr)) // [object Array]
- 闭包的更多作用
在实际开发中,闭包的运用非常广泛,这里仅例举少量示例。
1. 封装变量
闭包可以帮助把一些不需要暴露在全局的变量封装成“私有变量”。假设有一个计算乘积的简单函数
var mult = function(){
var a = 1;
for ( var i = 0, l = arguments.length; i < l; 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 ( mult( 1,2,3 ) ); // 输出:6
alert ( mult( 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 ];
}
return cache[ args ] = calculate.apply( null, arguments );
}
})();
- 2.延续局部变量的寿命
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;
}
})();
- 3.闭包和面向对象设计
对象以方法的形式包含了过程,而闭包则是在过程中以环境的形式包含了数据。
通常用面向对象思想能实现的功能,用闭包也能实现。反之亦然。可以使用闭包来实现一个完整的面向对象系统。
闭包代码:
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 = function(){
this.value = 0;
};
Extent.prototype.call = function(){
this.value++;
console.log( this.value );
};
var extent = new Extent();
extent.call(); // 1
extent.call(); // 2
extent.call(); // 3
- 4 用闭包实现命令模式
在完成闭包实现的命令模式之前,我们先用面向对象的方式来编写一段命令模式的代码
<body>
<div>
<button id='execute'>点击我执行命令</button>
<button id='undo'>点击我执行命令</button>
</div>
<script>
//demo1 面向对象实现命令模式
var TV={
open:function(){
console.log('打开电视机');
},
close:function(){
console.log("关上电视机");
}
};
var OpenTvCommand=function(receiver){
this.receiver=receiver;
};
OpenTvCommand.prototype.execute=function(){
this.receiver.open(); // 执行命令,打开电视机
};
OpenTvCommand.prototype.undo=function(){
this.receiver.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>
命令模式的意图是把请求封装为对象,从而分离请求的发起者和请求的接收者(执行者)之
间的耦合关系。在命令被执行之前,可以预先往命令对象中植入命令的接收者。
在面向对象版本的命令模式中,预先植入的命令接收者被当成对象的属性保存起来;而在闭包版本的命令模式中,命令接收者会被封闭在闭包形成的环境中,代码如下:
闭包实现命令模式:
<body>
<div>
<button id='execute'>点击我执行命令</button>
<button id='undo'>点击我执行命令</button>
</div>
<script>
// demo2 闭包实现命令模式
var TV={
open:function(){
console.log('打开电视机');
},
close:function(){
console.log("关上电视机");
}
};
var createCommand=function(receiver){
var execute=function(){
return receiver.open(); // 执行命令,打开电视机
}
var undo=function(){
return receiver.close(); // 执行命令,关闭电视机
}
return{
execute:execute,
undo:undo
}
};
var setCommand=function(command){
document.getElementById('execute').onclick=function(){
command.execute(); //输出:打开电视机
}
document.getElementById('undo').onclick=function(){
command.undo(); // 输出:关闭电视机
}
}
setCommand( createCommand(TV));
</script>
</body>
- 5 闭包与内存管理
使用闭包的一部分原因是我们选择主动把一些变量封闭在闭包中,因为可能在以后还需要使用这些变量,把这些变量放在闭包中和放在全局作用域,对内存方面的影响是一致的,这里并不能说成是内存泄露。如果在将来需要回收这些变量,我们可以手动把这些变量设为 null。
跟闭包和内存泄露有关系的地方是,使用闭包的同时比较容易形成循环引用,如果闭包的作用域链中保存着一些 DOM 节点,这时候就有可能造成内存泄露。