原文連結:周大俠啊 進擊的 JavaScript(四) 之 閉包
上一節說了執行上下文,這節我們們就乘勝追擊來搞搞閉包!頭疼的東西讓你不再頭疼!
一、函式也是引用型別的。
function f(){ console.log("not change") };
var ff = f;
function f(){ console.log("changed") };
ff();
//"changed"
//ff 儲存著函式 f 的引用,改變f 的值, ff也變了
//來個對比,估計你就明白了。
var f = "not change";
var ff = f;
f = "changed";
console.log(ff);
//"not change"
//ff 儲存著跟 f 一樣的值,改變f 的值, ff 不會變
複製程式碼
其實,就是引用型別 和 基本型別的 區別。
二、函式建立一個引數,就相當於在其內部宣告瞭該變數
function f(arg){
console.log(arg)
}
f();
//undefined
function f(arg){
arg = 5;
console.log(arg);
}
f();
//5
複製程式碼
三、引數傳遞,就相當於變數複製(值的傳遞)
基本型別時,變數儲存的是資料,引用型別時,變數儲存的是記憶體地址。引數傳遞,就是把變數儲存的值 複製給 引數。
var o = { a: 5 };
function f(arg){
arg.a = 6;
}
f(o);
console.log(o.a);
//6
複製程式碼
四、垃圾收集機制
JavaScript 具有自動垃圾收集機制,執行環境會負責管理程式碼執行過程中使用的記憶體。函式中,正常的區域性變數和函式宣告只在函式執行的過程中存在,當函式執行結束後,就會釋放它們所佔的記憶體(銷燬變數和函式)。
而js 中 主要有兩種收集方式:
- 標記清除(常見) //給變數標記為“進入環境” 和 “離開環境”,回收標記為“離開環境”的變數。
- 引用計數 // 一個引用型別值,被賦值給一個變數,引用次數加1,通過變數取得引用型別值,則減1,回收為次數為0 的引用型別值。
知道個大概情況就可以了,《JavaScript高階程式設計 第三版》 4.3節 有詳解,有興趣,可以看下。.
五、作用域
之前說過,JavaScript中的作用域無非就是兩種:全域性作用域和區域性作用域。 根據作用域鏈的特性,我們知道,作用域鏈是單向的。也就是說,在函式內部,可以直接訪問函式外部和全域性變數,函式。但是,反過來,函式外部和全域性,是訪問不了函式內的變數,函式的。
function testA(){
var a = 666;
}
console.log(a);
//報錯,a is not defined
var b = 566;
function testB(){
console.log(b);
}
//566
複製程式碼
但是,有時候,我們需要在函式外部 訪問函式內部的變數,函式。一般情況下,我們是辦不到的,這時,我們就需要閉包來實現了。
六、開始閉包!
function fa(){
var va = "this is fa";
function fb(){
console.log(va);
}
return fb;
}
var fc = fa();
fc();
//"this is fa"
複製程式碼
想要讀取fa
函式內的變數 va
,我們在內部定義了一個函式 fb
,但是不執行它,把它返回給外部,用 變數fc
接受。此時,在外部再執行fc
,就讀取了fa 函式內的變數 va
。
七、閉包的概念
其實,簡單點說,就是在 A 函式內部,存在 B 函式, B函式 在 A 函式 執行完畢後再執行。B執行時,訪問了已經執行完畢的 A函式內部的變數和函式。
由此可知:閉包是函式A的執行環境 以及 執行環境中的函式 B組合而構成的。
上篇文章中說過,變數等 都儲存在 其所在執行環境的活動物件中,所以說是 函式A 的執行環境。
當 函式A執行完畢後,函式B再執行,B的作用域中就保留著 函式A 的活動物件,因此B中可以訪問 A中的 變數,函式,arguments物件。此時產生了閉包。大部分書中,都把 函式B 稱為閉包,而在谷歌瀏覽器中,把 A函式稱為閉包。
八、閉包的本質
之前說過,當函式執行完畢後,區域性活動物件就會被銷燬。其中儲存的變數,函式都會被銷燬。記憶體中僅儲存全域性作用域(全域性執行環境的變數物件)。但是,閉包的情況就不同了。
以上面的例子來說,函式fb 和其所在的環境 函式fa,就組成了閉包。函式fa執行完畢後,按道理說, 函式fa 執行環境中的 活動物件就應該被銷燬了。但是,因為 函式fa 執行時,其中的 函式fb 被 返回,被 變數fc 引用著。導致,函式fa 的活動物件沒有被銷燬。而在其後 fc()
執行,就是 函式fb 執行時,構建的作用域中儲存著 函式fa 的活動物件,因此,函式fb 中 可以通過作用域鏈訪問 函式fa 中的變數。
我已經盡力地說明白了。就看各位的了。哈哈!其實,簡單的說:就是fa函式執行完畢了,其內部的 fb函式沒有執行,並返回fb的引用,當fb再次執行時,fb的作用域中保留著 fa函式的活動物件。
再來個有趣經典的例子:
for (var i=1; i<=5; i++) {
setTimeout(function(){
console.log(i);
},i*1000);
}
//每隔一秒輸出一個6,共5個。
複製程式碼
是不是跟你想的不一樣?其實,這個例子重點就在setTimeout函式上,這個函式的第一個引數接受一個函式作為回撥函式,這個回撥函式並不會立即執行,它會在當前程式碼執行完,並在給定的時間後執行。這樣就導致了上面情況的發生。
可以下面對這個例子進行變形,可以有助於你的理解把:
var i = 1;
while(i <= 5){
setTimeout(function(){
console.log(i);
},i*1000)
i = i+1;
}
複製程式碼
正因為,setTimeout
裡的第一個函式不會立即執行,當這段程式碼執行完之後,i
已經 被賦值為6
了(等於5
時,進入迴圈,最後又加了1
),所以 這時再執行setTimeout
的回撥函式,讀取 i
的值,回撥函式作用域內沒有i,向上讀取,上面作用域內i
的值就是6
了。但是 i * 1000
,是立即執行的,所以,每次讀的 i
值 都是對的。
這時候,就需要利用閉包來儲存每個迴圈時, i
不同的值。
function makeClosures(i){ //這裡就和 內部的匿名函式構成閉包了
var i = i; //這步是不需要的,為了讓看客們看的輕鬆點
return function(){
console.log(i); //匿名沒有執行,它可以訪問i 的值,儲存著這個i 的值。
}
}
for (var i=1; i<=5; i++) {
setTimeout(makeClosures(i),i*1000);
//這裡簡單說下,這裡makeClosures(i), 是函式執行,並不是傳參,不是一個概念
//每次迴圈時,都執行了makeClosures函式,都返回了一個沒有被執行的匿名函式
//(這裡就是返回了5個匿名函式),每個匿名函式都是一個區域性作用域,儲存著每次傳進來的i值
//因此,每個匿名函式執行時,讀取`i`值,都是自己作用域內儲存的值,是不一樣的。所以,就得到了想要的結果
}
//1
//2
//3
//4
//5
複製程式碼
閉包的關鍵就在,外部的函式執行完畢後,內部的函式再執行,並訪問了外部函式內的變數。
你可能在別處,或者自己想到了下面這種解法:
for (var i=1; i<=5; i++) {
(function(i){
setTimeout(function(){
console.log(i);
},i*1000);
})(i);
}
複製程式碼
如果你一直把這個當做閉包,那你可能看到的是不同的閉包定義吧(犀牛書和高程對閉包的定義不同)。嚴格來說,這不是閉包,這是利用了立即執行函式 和 函式作用域 來解決的。
做下變形,你再看看:
for (var i=1; i<=5; i++) {
function f(i){
setTimeout(function(){
console.log(i);
},i*1000);
};
f(i);
}
複製程式碼
這樣看就很明顯了吧,主要是利用了函式作用域,而使用立即執行函式,是為了簡化步驟。
總結:判斷是不是閉包,我總結了要滿足以下三點:
- 兩個函式。有內函式 和 外函式。
- 外函式執行完畢後,內函式 還沒有執行。
- 當內函式執行時(通過外部引用或者返回內函式),訪問了 外函式內部的 變數,函式等(說是訪問,其實內函式儲存著外函式的活動物件,因此,arguments物件也可以訪問到)。
附錄:
其實這道題,知道ES6
的 let
關鍵詞,估計也想到了另一個解法:
for (let i=1; i<=5; i++) { //這裡的關鍵就是使用的let 關鍵詞,來形成塊級作用域
setTimeout(function(){
console.log(i);
},i*1000);
}
複製程式碼
我不知道,大家有沒有疑惑啊,為啥使用了塊級作用域就可以了呢。反正我當初就糾結了半天。
11月 2日修正:
這個答案的關鍵就在於 塊級作用域的規則了。它讓let
宣告的變數只在{}
內有效,外部是訪問不了的。
做下變形,這個是為了方便理解的,事實並非如此:
for (var i=1; i<=5; i++) {
let j = i;
setTimeout(function(){
console.log(j);
},j*1000);
}
複製程式碼
當for 的()
內使用 let
時,for 迴圈就存在兩個作用域,()
括號裡的父作用域,和 {}
中括號裡的 子作用域。
每次迴圈都會建立一個 子作用域。儲存著父作用域傳來的值,這樣,每個子作用域內的值都是不同的。當setTimeout 的匿名函式執行時,自己的作用域沒有i
的值,向上讀取到了該 子作用域 的 i
值。因此每次的值才會不一樣。
上面用立即執行函式模擬塊級作用域,就是這個道理啦!