上一節說了執行上下文,這節我們就乘勝追擊來搞搞閉包!頭疼的東西讓你再也不頭疼!瀏覽器
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 不會變
其實,就是引用類型 和 基本類型的 區別。閉包
<br/>函數
function f(arg){ console.log(arg) } f(); //undefined function f(arg){ arg = 5; console.log(arg); } f(); //5
<br/>this
基本類型時,變量保存的是數據,引用類型時,變量保存的是內存地址。參數傳遞,就是把變量保存的值 複製給 參數。設計
var o = { a: 5 }; function f(arg){ arg.a = 6; } f(o); console.log(o.a); //6
<br/>code
JavaScript 具備自動垃圾收集機制,執行環境會負責管理代碼執行過程當中使用的內存。函數中,正常的局部變量和函數聲明只在函數執行的過程當中存在,當函數執行結束後,就會釋放它們所佔的內存(銷燬變量和函數)。對象
而js 中 主要有兩種收集方式:ip
知道個大概狀況就能夠了,《JavaScript高級程序設計 第三版》 4.3節 有詳解,有興趣,能夠看下。.內存
<br/>作用域
以前說過,JavaScript中的做用域無非就是兩種:全局做用域和局部做用域。
根據做用域鏈的特性,咱們知道,做用域鏈是單向的。也就是說,在函數內部,能夠直接訪問函數外部和全局變量,函數。可是,反過來,函數外部和全局,是訪問不了函數內的變量,函數的。
function testA(){ var a = 666; } console.log(a); //報錯,a is not defined var b = 566; function testB(){ console.log(b); } //566
可是,有時候,咱們須要在函數外部 訪問函數內部的變量,函數。通常狀況下,咱們是辦不到的,這時,咱們就須要閉包來實現了。
<br/>
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
。
<br/>
其實,簡單點說,就是在 A 函數內部,存在 B 函數, B函數 在 A 函數 執行完畢後再執行。B執行時,訪問了已經執行完畢的 A函數內部的變量和函數。
由此可知:閉包是函數A的執行環境 以及 執行環境中的函數 B組合而構成的。
上篇文章中說過,變量等 都儲存在 其所在執行環境的活動對象中,因此說是 函數A 的執行環境。
當 函數A執行完畢後,函數B再執行,B的做用域中就保留着 函數A 的活動對象,所以B中能夠訪問 A中的 變量,函數,arguments對象。此時產生了閉包。大部分書中,都把 函數B 稱爲閉包,而在谷歌瀏覽器中,把 A函數稱爲閉包。
<br/>
以前說過,當函數執行完畢後,局部活動對象就會被銷燬。其中保存的變量,函數都會被銷燬。內存中僅保存全局做用域(全局執行環境的變量對象)。可是,閉包的狀況就不一樣了。
以上面的例子來講,函數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); }
這樣看就很明顯了吧,主要是利用了函數做用域,而使用當即執行函數,是爲了簡化步驟。
總結:判斷是否是閉包,我總結了要知足如下三點:
<br/>
<br/>
其實這道題,知道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
或者 var
建立一個新變量,並以以前迭代中同名變量的值將其初始化。而這裏正由於使用let,致使每次循環都會建立一個新的塊級做用域,這樣,雖然setTimeout 中的匿名函數內沒有 i
值,但它向上做用域讀取i
值,就讀到了塊級做用域內 i
的值。
上面用當即執行函數模擬塊級做用域,就是這個道理啦!