在介紹閉包之前,首先解釋在隨後的測試例項中會使用的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個變數。