最近看了幾篇有關javascript閉包的文章,包括最近正火的湯姆大叔系列,還有《javascript進階程式設計》中的文章,……我看不懂,裡面有些代碼是在大學教科書中看都沒看過的,天書一般。幸好最近遇到兩本好書《ppk on javascript》和《object-oriented JavaScript》,正字閱讀中,後者還沒有中文版,但前者還是建議看原版,寫的不複雜,有興趣的朋友可以看看,適合想進階的朋友。
今天就結合這兩本書,用最淺顯的語言和最通俗的方式談談javascript中的閉包,因為也是新手,所以有有誤的地方請各位指出,謝謝
一. 準備知識
1.函數作為函數的參數
在學習javascript中,你始終要有一個有學習與其他語言不同的概念:函數(function)不麼特殊的東西,它也是一種資料,與bool ,string,number沒有什麼兩樣。
函數的參數可以string,number,bool如:
function(a, b) {return a + b;}
但同樣也可以傳入函數。對你沒有聽錯,函數的參數是函數!加入你有以下兩個函數:
複製代碼 代碼如下://把三個數翻一倍
function multiplyByTwo(a, b, c) {
var i, ar = [];
for(i = 0; i < 3; i++) {
ar[i] = arguments[i] * 2;
}
return ar;
}
複製代碼 代碼如下://把數加一
function addOne(a) {
return a + 1;
}
然後這麼使用 複製代碼 代碼如下:var myarr = [];
//先把每個數乘以二,用了一個迴圈
myarr = multiplyByTwo(10, 20, 30);
//再把每個數加一,又用了一個迴圈
for (var i = 0; i < 3; i++) {myarr[i] = addOne(myarr[i]);}
要注意到其實這個過程用了兩個迴圈,還是有提升的空間的,不如這麼做: 複製代碼 代碼如下:function multiplyByTwo(a, b, c, addOne) {
var i, ar = [];
for(i = 0; i < 3; i++) {
ar[i] = addOne (arguments[i] * 2);
}
return ar;
}
這樣就把函數當做參數傳遞進去了,並且在第一個迴圈中直接調用。這樣的函數就是著名的回呼函數(Callback function)
2.函數作為傳回值
在函數中可以有傳回值,但是我們一般都熟悉數值的返回,如 複製代碼 代碼如下:function ex(){
return 12
}
但你一旦意識到函數只是一種資料的話,你就可以想到同樣可以返回函數。注意看下面這個函數: 複製代碼 代碼如下:function a() {
alert('A!');
return function(){
alert('B!');
};
}
它返回了一個彈出”B!”的函數。接下來使用它: 複製代碼 代碼如下:var newFunc = a();
newFunc();
結果是什麼呢?首先執行a()的時候,彈出”A!”,此時newFunc接受了a的傳回值,一個函數——此時newFunc就變成了那個被a返回的函數,再執行newFunc時,彈出”B!”
3.javascript的範圍
javascript的範圍很特別,它是以函數為單位的,而不是像其他語言以塊為單位(如一個迴圈中),看下面這個例子:
var a = 1; function f(){var b = 1; return a;}
如果你此時試圖想得到b的值:在firebug中試圖輸入alert(b)的話,你會得到錯誤提示:
b is not defined
為什麼你可以這麼理解:你所在的編程環境或者視窗是最頂級的一個函數,好像一個宇宙,但是b只是在你內建函式的一個變數,宇宙中的小星球上的一個點,你很難找到它,所以在這個環境中你不能調用它的;反之這個內建函式可以調用變數a,因為它暴露在整個宇宙中,無處藏身,同時也可以調用b,因為它就在自己的星球上,函數內部。
就上面這個例子說:
在f()外,a可見,b不可見
在f()內,a可見,b也可見
再複雜點: 複製代碼 代碼如下:var a = 1; //b,c在這一層都不可見
function f(){
var b = 1;
function n() { //a,b,c對這個n函數都可以調用,因為a,b暴露在外,c又是自己內部的
var c = 3;
}
}
問你,函數b可以調用變數c嗎?不行,記住javascript的範圍是以函數為單位的,c在n的內部,所以對f來說是不可見的。
開始正式談閉包:
首先看這個圖:
假設G,F,N 分別代表三個層次的函數,層次,a,b,c分別是其中的變數。根據上面談到的範圍,我們有如下結論:
- 如果你在a點,你是不可以引用b的,因為b對你是不可見的
- 只有c可以引用b
閉包的弔詭之處的就在於發生了如下情況:
N突破了F的限制!跑到於a同一層了!因為函數只認它們在定義時所處的環境(而不是執行時,這點很重要),N中的c仍然可以訪問b!此時的a還是不可以訪問b!
但是這是怎麼實現的呢?如下:
閉包1:
複製代碼 代碼如下:function f(){
var b = "b";
return function(){ //沒有名字的函數,所以是匿名函數
return b;
}
}
注意返回的函數可以訪問它父親函數中的變數b
此時如果你想取b的值,當然是undefined
但是如果你這麼做: 複製代碼 代碼如下:var n = f();
n();
你可以取到b的值了!雖然此時n函數在f的外面,b又屬於f內部的變數,但是f內部出了一個內鬼,返回了b的值……
現在大家有點感覺了吧
閉包2: 複製代碼 代碼如下:var n;
function f(){
var b = "b";
n = function(){
return b;
}
}
如果此時調用f會怎麼樣?那就產生了一個n的全域範圍函數,但是它卻能訪問f的內部,照樣返回b的值,與上面有異曲同工之妙!
閉包3:
你還可以用閉包訪問函數的參數 複製代碼 代碼如下:function f(arg) {
var n = function(){
return arg;
};
arg++;
return n;
}
此時如果使用: 複製代碼 代碼如下:var m = f(123);
m();
結果是124
因為此時f中返回的匿名函數經過了兩道轉手,先給n,再賦給外面的m,但本質沒有變,把定義時父函數的參數返回了
閉包4: 複製代碼 代碼如下:var getValue, setValue;
function() {
var secret = 0;
getValue = function(){
return secret;
};
setValue = function(v){
secret = v;
};
})
運行: 複製代碼 代碼如下:getValue()
0
setValue(123)
getValue()
123
這個就不用解釋了吧,如果你有物件導向語言基礎的話(如C#),這裡的getValue和setValue就類似於一個對象的屬性訪問器,你可以通過這兩個訪問器來賦值和取值,而不是能訪問其中內容
其實書中還有幾個閉包的例子,但是原理用上面四個就足夠了,希望能起拋磚引玉的作用,給javascript進階者對閉包有一個更深刻的理解