本文內容:
函數內部訪問全局變量
函數外部不能直接訪問局部變量
函數外部訪問局部變量
保護私有成員
持久性
模塊化
抽象性
閉包是 JavaScript 的重要特性,非常強大,可用於執行復雜的計算,可並不容易理解,尤其是對之前從事面向對象編程的人來說,對 JavaScript 認識和編程顯得更難。特別是在看一些開源的 JavaScript 代碼時感覺尤其如此,跟看天書沒什麼區別。
一般情況下,人們認識一個事物,會根據之前的經驗,進行對比和總結,在腦中建立一個模型,從而理解掌握它,但是 JavaScript 與面向對象編程實在“沒有可比性”,最明顯的是某過於語法,總覺得“怪怪的”,更不用說,一些高級特性。
因此,從閉包可以做什麼開始,將有助於進一步理解閉包。
函數內部訪問全局變量
函數內部可以訪問全局變量,如下所示:
var baz = 10;
function foo() {
alert(baz);
}
foo();
函數外部不能直接訪問局部變量
函數外部不能訪問局部變量,下面代碼將報錯:
function foo() {
var bar = 20;
}
alert(bar);
會報錯,因為 bar 是局部變量。函數內部聲明的變量,若沒有使用 var 關鍵字,則變量是一個全局變量。
function foo() {
bar = 20;
}
alert(bar);
函數外部訪問局部變量
實際情況是,我們需要從函數外部訪問函數內部的局部變量。大多數情況只是讀,而不能修改,修改對代碼是不安全的,如下所示:
function foo() {
var a = 10;
function bar() {
a *= 2;
}
bar();
return a;
}
var baz = foo(); // 20
// baz(); 不用再調用
其中,a 是 foo 的局部變量,bar 當然可以訪問。但如何在 foo 外,調用 bar,並訪問 a?另外,此時,baz 不是一個對象,只是一個值,因此,不能再像 baz() 這樣調用,也因此只能改變一次 a 的值。
現在,改寫上面的代碼,如下所示:
<script type="text/javascript">
function foo() {
var a = 10;
function bar() {
a *= 2;
return a;
}
return bar;
}
var baz = new foo();
document.writeln( baz()); // 20
document.writeln( baz()); // 40
document.writeln( baz()); // 80
var blat = new foo();
document.writeln(blat()); // 20
document.writeln(blat()); // 40
document.writeln(blat()); // 80
document.writeln(blat()); // 160
</script>
說明:
現在可以從外部訪問 a;
JavaScript 的作用域是詞法性的。a 是運行在定義它的 foo 中,而不是運行在調用 foo 的作用域中。因此,只要 bar 定義在 foo 中,bar 就能訪問 foo 的局部變量 a,即使 foo 已經執行完畢。也就是說,var baz = foo() 執行後,foo 已經執行結束了,局部變量 a 應該不存在了,但之後再調用 baz 發現,a 依然存在——這就是 JavaScript 特色,運行在定義中,而不是運行在調用。這跟 C、C#以及其他編程語言明顯不同。閉包,就是在 foo 執行完並返回後,使得 Javascript 垃圾回收機制不收回 foo 所占用的資源,因為 foo 的內部函數 bar 的執行需要 foo 中的變量;
var baz = foo() 是 bar 一個引用;var blat= foo() 是另一個。
持久性
在“函數外部訪問函數內部的局部變量”小節,我們看到,函數被調用多次,私有變量能夠保持其持久性,如示例 4 所示,因為,JavaScript 是運行在定義中,而不是運行在調用中的。盡管,變量的作用域僅限於其函數內,無法從外部直接訪問,但在多次調用期間,值仍然存在。
因此,閉包可以用來信息隱藏,並應用於需要狀態表達的某些編程范型中。
保護私有成員
在以上描述,通過閉包可以創建只允許特定的函數訪問變量,而且變量在函數的調用期間依然存在。
var foo = (function () {
var data = {};
return function (key, val) {
if (val == undefined) { // get
return data[key];
}
else { // set
return data[key] = val;
}
}
})();
foo('x'); // x 為 undefined
foo('x', 1); // set
foo('x'); // get
說明:
data 是私有變量,從外部不能直接訪問 data,它是隱藏的;
創建一個函數,這個函數提供一些訪問 data 的方法。
甚至可以寫出類似“面向對象”的效果,如下所示:
var Book = function (newIsbn, newTitle, newAuthor) {
// 私有屬性
var isbn, title, author;
// 私有方法
function checkIsbn(isbn) {
// TODO
}
// 特權方法
this.getIsbn = function () {
return isbn;
};
this.setIsbn = function (newIsbn) {
if (!checkIsbn(newIsbn)) throw new Error('Book: Invalid ISBN.');
isbn = newIsbn;
};
this.getTitle = function () {
return title;
};
this.setTitle = function (newTitle) {
title = newTitle || 'No title specified.';
};
this.getAuthor = function () {
return author;
};
this.setAuthor = function (newAuthor) {
author = newAuthor || 'No author specified.';
};
// 構造器代碼
this.setIsbn(newIsbn);
this.setTitle(newTitle);
this.setAuthor(newAuthor);
};
// 共有、非特權方法
Book.prototype = {
display: function () {
// TODO
}
};
說明:
用 var 聲明變量 isbn、title 和 author,而不是 this,意味著它們只存在 Book 構造器中。checkIsbn 函數也是,因為它們是私有的;
訪問私有變量和方法的方法只需聲明在 Book 中即可。這些方法稱為特權方法。因為,它們是公共方法,但卻能訪問私有變量和私有方法,像 getIsbn、setIsbn、getTitle、setTitle、getAuthor、setAuthor(取值器和構造器)。
為了能在對象外部訪問這些特權方法,這些方法前邊加了 this 關鍵字。因為這些方法定義在 Book 構造器的作用域裡,所以它們能夠訪問私有變量 isbn、title 和 author。但在這些特權方法裡引用 isbn、title 和 author 變量時,沒有使用 this 關鍵字,而是直接引用。因為它們不是公開的。
任何不需要直接訪問私有變量的方法,像 Book.prototype 中聲明的,如 display。它不需要直接訪問私有變量,而是通過 get*、set* 間接訪問。
這種方式創建的對象可以具有真正私有的變量。其他人不能直接訪問 Book 對象的任何內部數據,只能通過賦值器。這樣一切盡在掌握。但這種方式的缺點是: “門戶大開型”對象創建模式中,所有方法都創建在原型 prototype 對象中,因此不管生成多少對象實例,這些方法在內存中只有一份。而采用本節的做法,每生成一個新的對象實例,都將為每個私有方法(如,checkIsbn)和特權方法(如,getIsbn、setIsbn、getTitle、setTitle、getAuthor、setAuthor)生成一個新的副本。
因此,本節方法,只適於用在真正需要私有成員的場合。另外,這種方式也不利於繼承。
模塊化——遍歷集合
閉包有益於模塊化編程。它能以簡單方式開發較小的模塊,從而提高開發速度和程序的可復用性。與沒有使用閉包的程序相比,使用閉包可將模塊劃分得更小。例如,計算數組中所有數字的和,只需循環遍歷數組相加即可。但現在若要計算所有數字的積?打印所有數字?這些問題都需要遍歷數組,若采用閉包,就不得不反復寫循環語句,而這在 JavaScript 中就不用。如 jQuery 的 each 方法。
var each = function (object, callback, args) {
//當需要遍歷的是一個對象時,name變量用於記錄對象的屬性名
var name;
//當需要遍歷的是一個數組時,i變量用於記錄循環的數組下標
var i = 0;
//遍歷數組長度,當需要遍歷的對象是一個數組時存儲數組長度
//如果需要遍歷的是一個對象,則length === undefined
var length = object.length;
//檢查第1個參數object是否是一個對象
//根據object.length排除數組類型,根據isFunction排除函數類型(因為函數也是對象)
var isObj = length === undefined || typeof (object) == "function";
//回調函數具有附加參數時,執行第一個分支
//if(!!args) {
if (args) {
//需要遍歷的是一個對象
if (isObj) {
//遍歷對象屬性,name是對象的屬性名,再函數頂部已聲明
//許多人不太習慣for(var name in object)方式,如果不進行聲明,則name就會被定義為全局變量
for (name in object) {
//調用callback回調函數,且回調函數的作用域表示為當前屬性的值
//如:callback() { this; //函數中的this指向當前屬性值
//將each的第3個參數args作為回調函數的附加參數
if (callback.apply(object[name], args) === false) {
//如果在callback回調函數中使用return false;則不執行下一次循環
break;
}
}
}
//需要遍歷的是一個數組
else {
//循環長度,循環變量i在函數頂部已定義
//循環變量的自增在循環內部執行
for (; i < length; ) {
//調用callback函數,與上面注釋的callback調用一致
//此處callback函數中的this指向當前數組元素
if (callback.apply(object[i++], args) === false) {
break;
}
}
}
}
//回調函數沒有附加參數時,執行第二個分支
else {
//需要遍歷的是一個對象
if (isObj) {
//循環對象的屬性名,name在函數頂部已定義
for (name in object) {
//調用callback回調函數
//在不帶參數的對象遍歷中,作用域表示為當前屬性的值
//且回調函數包含兩個參數,第一個數當前屬性名,第二個是當前屬性值
//我覺得這句代碼修改一下會更好用:if(callback.call(object, name, object[name]) === false) {
if (callback.call(object[name], name, object[name]) === false) {
//如果在callback回調函數中使用return false;則不執行下一次循環
break;
}
}
}
//需要遍歷的是一個數組
else {
//這裡的for寫法有點BT,解釋為:
//var value = object[0];
//for(; i < length;) {
// if(false === callback.call(value, i, value)) {
// break;
// }
// value = object[++i];
//}
//同樣,我覺得這裡的代碼稍加修改會更好用:
//for (; i < length && false !== callback.call(object, i, object[i++]);) {
//}
for (var value = object[0]; i < length && callback.call(value, i, value) !== false; value = object[++i]) {
}
}
}
//這裡返回遍歷的對象或數組,但object沒有被更改,因此一般不給$.each()賦值
//但是如果按照我在注釋中所修改的寫法來使用,且在callback回調函數中對this(即對object的引用)進行了修改
//則這裡返回的object是被修改後的對象或數組
return object;
}
var arr = [1, 2, 3, 4, 5];
each(arr, function (index, value) {
alert(index + ':' + value);
});
抽象性
從上面可以看到,閉包可以封裝數據和行為,具有較好抽象能力,可以用來實現面向對象的委托和接口。
另外,閉包也簡化了代碼,一個常見問題是,窗口上有個按鈕,當點擊按鈕時會產生事件。如果在按鈕中處理這個事件,那就必須在按鈕中保存處理這個事件時所需的各個對象的引用。另一個選擇是把這個事件轉發給父窗口,由父窗口來處理,或使用監聽者模式。無論哪種方式,都不太方便,甚至要借助一些工具來幫助生成事件處理的代碼框架。用閉包來處理就比較方便了,可以在按鈕裡直接寫事件處理代碼。