闭包的形成与变量的作用域以及变量的生存周期密切相关。
变量的生存周期
对于全局变量来说,全局变量的生存周期当然是永久的,除非我们主动销毁这个变量。而对于在函数内用var关键字声明的局部变量来说,当退出函数时,这些局部变量就失去了他们的价值。
var func = function () {
var a = 1; // 退出函数后局部变量a将被销毁
alert(a);
};
func();
现在来看看下面的代码: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一直处在这个环境里。既然局部变量所在的环境还能被外界访问,这个局部变量就有了不被销毁的理由。
在这里产生了一个闭包结构,局部变量的生命看起来被延续了。
下面介绍一个闭包的经典应用。代码如下:
<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; i < nodes.length; i++){
nodes[i].onclick = function () {
alert(i);
}
}
</script>
</body>
测试代码发现,无论点击哪个div,弹出的都是5。这是因为div节点的onclick事件是被异步触发的,当事件被触发的时候,此时变量i的值已经是5,所以在div的onclick事件函数中顺着作用域链从内到外查找变量i时,查到的总是5。
解决的方法是在闭包的帮助下,把每次循环的i值都封闭起来。当在事件函数中顺着作用域链中从内到外查找变量i时,会先找到被
封闭在闭包环境中的i,如果有5个div,这里i就分别是01234
for (var i = 0; i < nodes.length; i++){
(function (i) {
nodes[i].onclick = function () {
alert(i);
}
})(i)
}
1.封装变量
闭包可以帮忙把一些不需要暴露在全局的变量封装成“私有变量”。假设有一个计算乘积的简单函数:
var mult = function () {
var a = 1;
for (var i = 0; i < arguments.length; 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; i < arguments.length; 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 (cache[args]){
return cache[args];
}
var a = 1;
for (var i = 0; i < arguments.length; 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; i < arguments.length; 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;
}
})();
闭包和面向对象设计可以使用闭包来实现一个完整的面向对象系统。
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();
extent.call();
extent.call();
或者: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();