在介绍闭包之前,首先解释在随后的测试实例中会使用的assert测试函数,这个方法有别于alert()测试,有很大的改进。
assert()测试方法
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<style>
#results li.pass { color: green; }
#results li.fail { color: red; }
</style>
</head>
<body>
<ul id="results"></ul>
<script type="text/javascript">
function assert( value, desc ) {
var li = document.createElement("li");
li.className = value ? "pass" : "fail";//红色代表错误,绿色代表成功
li.appendChild( document.createTextNode( desc ) );
document.getElementById("results").appendChild( li );
}
var outerValue = 'nanjia';
assert(outerValue == 'nanjia', "i can see the nanjia");
</script>
</body>
</html>
用法:assert(condition, “string”); 把字符串显示在页面上,根据条件是否成立决定显示的颜色,条件为真就显示为绿色,否则为红色。
闭包
闭包的特点就是内部匿名函数可以访问外部函数作用域的变量和方法(变量对象)。一般来说,当函数执行完毕后, 局部活动对象(函数的变量对象)就会被销毁,内存中仅保留全局作用域(全局执行环境的变量对象)。但是闭包创建时,内部函数的作用域链中会一直引用着外部函数的活动对象,这个活动对象一直被引用而不能被回收,一直占用内存,容易造成内存泄漏。
闭包的主要表现形式就是匿名函数,但是两者并不是等价的。闭包的常见用法就是封装一些私有变量,也就是限制这些变量的作用域。任何在函数中创建的变量都是私有变量,因为在函数外部不能访问这些变量,私有变量包括函数的参数、局部变量、在函数内部定义的其他函数。有权在外部访问私有变量的公有方法被称为特权方法,第一种创建特权方法的的方式是在构造函数中定义特权方法,如下:
function Ninjia(){
//私有属性
var feints = 0;
//特权方法
this.getFeints = function(){
return feints;
};
this.feintNum = function(){
feints++;
};
}
var ninjia= new Ninjia();
ninjia.feintNum();
assert(ninjia.getFeints() == 1, "can accesss interal variable");//可以通过特权方法访问
assert(ninjia.feints, "can access private variable");//无法访问
可以看出,特权方法之所以能够访问构造函数中定义的所有变量和函数,根本原因在于,特权方法就是一个闭包,可以通过作用域链访问外部函数,也就是构造函数。通过构造函数定义特权方法的缺点在于使用构造函数创建自定义对象的固有弊端,那就是方法(函数对象)的重复创建。
第二种方式是在私有作用域中定义私有变量或私有函数,同样也可以创建特权方法。
首先介绍私有作用域。JavaScript中是没有块级作用域的,为了在JavaScript中引入块级作用域,可以使用匿名函数模拟块级作用域。为什么大费周折整出块级作用域呢?因为有了块级作用域,每个开发人员都可以在块级作用域中定义自己的变量,而不用担心会搞乱全局作用域,过多的全局作用域变量核函数会导致命名冲突。匿名函数用作块级作用域被称为私有作用域(private scope),这个匿名函数实际上就是一个闭包,它可以访问自己内部活动对象,函数执行完成即销毁,不占用内存,语法如下:
(function () {
//块级作用域
})();
上述代码首先将函数声明包含在圆括号中,这表示这段代码实际上是一个函数表达式,紧随其后的圆括号会立即调用这个函数。
以下实例表明,私有作用域的变量不可由外部访问。
var outputNum = function(count){
(function(){
for (var i=0; i <count; i++){
alert(i);//i= 0,1,2,3,4
}
})();
alert(i);//Uncaught ReferenceError: i is not defined
};
outputNum(5);
然后介绍往私有作用域中添加私有变量和函数,同时定义特权方法。
(function(){
//私有属性
var name="";
//构造函数
Person = function(value){
name = value;
};
//构造函数原型方法-->特权方法
Person.prototype.getName = function(){
return name;
}
Person.prototype.setName = function(value){
name = value;
}
})();
var person1= new Person("zhang");
alert(person1.getName());//zhang
person1.setName("wang");
alert(person1.getName());//wang
var person2= new Person("li");
alert(person1.getName());//li,实例间的属性共享
这种方法的主要弊端在于,私有变量实际上变成了所有静态的由所有实例共享的属性,也就是说,在一个实例上调用方法改变私有变量值后,会在另一个变量上体现出来。
闭包的使用实例:
1)Ajax回调函数中的闭包:
jQuery('#testButton').click(function(){ //#1
var elem$ = jQuery("#testSubject"); //#2
elem$.html("Loading..."); //#3
jQuery.ajax({
url: "test.html",
success: function(html){ //#4
assert(elem$, //elem$是外部变量,匿名函数式回调函数
"We can see elem$, via the closure for this callback.");
elem$.html(html);
}
});
});
2)计时器回调函数中的闭包:
function animateIt(elementId){
var elem = document.getElementById(elementId);
var tick = 0;
var timer = setInterval(function(){
if (tick <100){
elem.style.top = tick+"px";
elem.style.left = tick+"px";
tick++;
}
else{
clearInterval(timer);
assert(tick == 100, "Tick accessd via a closure");
assert(elem, "elem accesse via a closure");
assert(timer, "timer reference accessed via a closure");
}
}, 10);
}
animateIt('box');
通过在函数内部定义变量(本例中是elem, tick, timer),并依靠闭包,可以在计时器回调函数调用的时候进行使用,这样,每个动画就有自己的私有变量(elem, tick, timer一次定义,多次使用)。如果在全局作用域中设置变量并且有多个动画需要设置,为了避免混淆,需要为每个动画设置3个变量。