上一節說了執行上下文,這節我們就乘勝追擊來搞搞閉包!頭疼的東西讓你再也不頭疼!瀏覽器
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 不會變
複製代碼
其實,就是引用類型 和 基本類型的 區別。bash
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 中 主要有兩種收集方式:ui
知道個大概狀況就能夠了,《JavaScript高級程序設計 第三版》 4.3節 有詳解,有興趣,能夠看下。.this
以前說過,JavaScript中的做用域無非就是兩種:全局做用域和局部做用域。 根據做用域鏈的特性,咱們知道,做用域鏈是單向的。也就是說,在函數內部,能夠直接訪問函數外部和全局變量,函數。可是,反過來,函數外部和全局,是訪問不了函數內的變量,函數的。spa
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
。code
其實,簡單點說,就是在 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);
}
複製代碼
這樣看就很明顯了吧,主要是利用了函數做用域,而使用當即執行函數,是爲了簡化步驟。
總結:判斷是否是閉包,我總結了要知足如下三點:
其實這道題,知道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
值。所以每次的值纔會不同。
上面用當即執行函數模擬塊級做用域,就是這個道理啦!