您的位置:首页 > 其它

3.1-闭包

2016-02-16 15:38 183 查看
参考书籍: Javascript设计模式与开发实践(曾探)

一、变量的作用域

变量的作用域,就是指变量的有效范围。

当在函数中声明一个变量的时候,如果该变量前面没有带上关键字var,这个变量就会成为全局变量,这当然是一种容易造成命名冲突的做法。

另外一种情况是用var 关键字在函数中声明变量,这时候的变量即是局部变量,只有在该函数内部才能访问到这个变量,在函数外面是访问不到的。

在JavaScript 中,函数可以用来创造函数作用域。此时的函数像一层半透明的玻璃,在函数里面可以看到外面的变量,而在函数外面则无法看到函数里面的变量。这是因为当在函数中搜索一个变量的时候,如果该函数内并没有声明这个变量,那么此次搜索的过程会随着代码执行环境创建的作用域链往外层逐层搜索,一直搜索到全局对象为止。变量的搜索是从内到外而非从外到内的。

//e.g.1
var func = function () {
var a = 1;
console.log("a: " + a); // 输出: 1
};

func();
console.log(a); // 输出:Uncaught ReferenceError: a is not defined

//e.g.2
var aa = 1;
var func1 = function () {
var b = 2;
var func2 = function () {
var c = 3;
console.log("b: " + b);  // 输出:2
console.log("c: " + aa); // 输出:1
};

func2();

//console.log(c); // 输出:Uncaught ReferenceError: c is not defined
};

func1();


二、变量的生存周期

除了变量的作用域之外,另外一个跟闭包有关的概念是变量的生存周期。

对于全局变量来说,全局变量的生存周期当然是永久的,除非我们主动销毁这个全局变量。

而对于在函数内用var 关键字声明的局部变量来说,当退出函数时,这些局部变量即失去了它们的价值,它们都会随着函数调用的结束而被销毁:

var func = function () {
var a3 = 1; // 退出函数后局部变量a 将被销毁
console.log("a3: " + a3);
};

func();

//现在来看看下面这段代码:
var func = function () {
var a4 = 1;

return function () {
a4++;
console.log("a4:" + a4);
}
};

var f = func();

f(); // 输出:2
f(); // 输出:3
f(); // 输出:4
f(); // 输出:5


跟我们之前的推论相反,当退出函数后,局部变量a 并没有消失,而是似乎一直在某个地方存活着。这是因为当执行var f = func();时,f返回了一个匿名函数的引用,它可以访问到func() 被调用时产生的环境,而局部变量a 一直处在这个环境里。既然局部变量所在的环境还能被外界访问,这个局部变量就有了不被销毁的理由。在这里产生了一个闭包结构,局部变量的生命看起来被延续了。

//一个闭包的经典应用
var nodes = document.getElementsByTagName('h5');
for (var i = 0, len = nodes.length; i < len; i++) {
nodes[i].onclick = function () {
console.log("i: " + i);
}
}


测试这段代码就会发现,无论点击哪个div,最后弹出的结果都是5。这是因为div 节点的onclick 事件是被异步触发的,当事件被触发的时候,for 循环早已结束,此时变量i 的值已经是5,所以在div 的onclick 事件函数中顺着作用域链从内到外查找变量i 时,查找到的值总是5。

解决方法是在闭包的帮助下,把每次循环的i 值都封闭起来。当在事件函数中顺着作用域链中从内到外查找变量i 时,会先找到被封闭在闭包环境中的i,如果有5 个div,这里的i 就分别是0,1,2,3,4:

for (var i = 0, len = nodes.length; i < len; i++) {
(function (i) {
nodes[i].onclick = function () {
console.log("i: " + i);
}
})(i)
}


根据同样的道理,我们还可以编写如下一段代码:

var Type = {};
for (var i = 0, type; type = ['String', 'Array', 'Number'][i++];) {
(function (type) {
Type['is' + type] = function (obj) {
return Object.prototype.toString.call(obj) === '[object ' + type + ']';
}
})(type)
}

console.log(Type.isArray([]));     // 输出:true
console.log(Type.isString("str")); // 输出:true


三、闭包的更多作用

1、封装变量

闭包可以帮助把一些不需要暴露在全局的变量封装成“私有变量”。

//计算乘积的简单函数
var mult = function () {
var a = 1;
for (var i = 0, l = arguments.length; i < l; i++) {
a = a * arguments[i];
}
return a;
};

console.log("mult: " + mult(2, 3));    // mult: 6
console.log("mult: " + mult(2, 3, 4)); // mult: 24


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

var mult2 = (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;
}
})();

console.log("mutl2: " + mult2(2, 3)); //mutl2: 6
console.log("mutl2: " + mult2(2, 3)); //mutl2: 6


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

var mult3 = (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);
}
})();

console.log("mult3: " + mult3(4, 5)); //mult3: 20


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;
}
})();


四、闭包和面向对象设计

过程与数据的结合是形容面向对象中的“对象”时经常使用的表达。对象以方法的形式包含了过程,而闭包则是在过程中以环境的形式包含了数据。通常用面向对象思想能实现的功能,用闭包也能实现。反之亦然。

//闭包方式的写法
var extent = function () {
var value = 0;

return {
call: function () {
value++;
console.log("value:" + value);
}
}
};

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

//面向对象的写法,就是:
var extent2 = {
value: 0,
call: function () {
this.value++;
console.log("extent2: " + this.value);
}
};
extent2.call(); // 输出:1
extent2.call(); // 输出:2
extent2.call(); // 输出:3

//或者:
var Extent = function () {
this.value = 0;
};

Extent.prototype.call = function () {
this.value++;
console.log("Extent: " + this.value);
};

var extent3 = new Extent();
extent3.call();
extent3.call();
extent3.call();


五、用闭包实现命令模式

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

<script>

//用面向对象的方式来编写一段命令模式的代码

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>
</html>


命令模式的意图是把请求封装为对象,从而分离请求的发起者和请求的接收者(执行者)之间的耦合关系。在命令被执行之前,可以预先往命令对象中植入命令的接收者。

但在JavaScript 中,函数作为一等对象,本身就可以四处传递,用函数对象而不是普通对象来封装请求显得更加简单和自然。如果需要往函数对象中预先植入命令的接收者,那么闭包可以完成这个工作。在面向对象版本的命令模式中,预先植入的命令接收者被当成对象的属性保存起来;而在闭包版本的命令模式中,命令接收者会被封闭在闭包形成的环境中,代码如下:

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 ) );


六、闭包与内存管理

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

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

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

同样,如果要解决循环引用带来的内存泄露问题,我们只需要把循环引用中的变量设为null即可。将变量设置为null 意味着切断变量与它此前引用的值之间的连接。当垃圾收集器下次运行时,就会删除这些值并回收它们占用的内存。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: