摘要:此時產生了閉包。導致,函數的活動對象沒有被銷毀。是不是跟你想的不一樣其實,這個例子重點就在函數上,這個函數的第一個參數接受一個函數作為回調函數,這個回調函數并不會立即執行,它會在當前代碼執行完,并在給定的時間后執行。
上一節說了執行上下文,這節咱們就乘勝追擊來搞搞閉包!頭疼的東西讓你不再頭疼!
一、函數也是引用類型的。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 或者 var 創建一個新變量,并以之前迭代中同名變量的值將其初始化。而這里正因為使用let,導致每次循環都會創建一個新的塊級作用域,這樣,雖然setTimeout 中的匿名函數內沒有 i 值,但它向上作用域讀取i 值,就讀到了塊級作用域內 i 的值。
上面用立即執行函數模擬塊級作用域,就是這個道理啦!
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/96109.html
摘要:為了更好的理解,在閱讀此文之前建議先閱讀上一篇進擊之詞法作用域與作用域鏈什么是閉包閉包的含義就是閉合,包起來,簡單的來說,就是一個具有封閉功能與包裹功能的結構。在中函數構成閉包。 為了更好的理解,在閱讀此文之前建議先閱讀上一篇《進擊JavaScript之詞法作用域與作用域鏈》 1.什么是閉包 閉包的含義就是閉合,包起來,簡單的來說,就是一個具有封閉功能與包裹功能的結構。所謂的閉包就是...
摘要:匿名函數是不能單獨寫的,所以就提不上立即執行了。六立即執行函數在閉包中的應用立即執行函數能配合閉包保存狀態。來看下上節內容中閉包的例子現在,我們來利用立即執行函數來簡化它第一個匿名函數執行完畢后,返回了第二個匿名函數。 前面的閉包中,提到與閉包相似的立即執行函數,感覺兩者還是比較容易弄混吧,嚴格來說(因為犀牛書和高程對閉包的定義不同),立即執行函數并不屬于閉包,它不滿足閉包的三個條件。...
摘要:如下代碼輸出的結果是代碼執行分為兩個大步預解析的過程代碼的執行過程預解析與變量聲明提升程序在執行過程中,會先將代碼讀取到內存中檢查,會將所有的聲明在此進行標記,所謂的標記就是讓解析器知道有這個名字,后面在使用名字的時候不會出現未定義的錯誤。 showImg(https://segmentfault.com/img/remote/1460000012922850); 如下代碼輸出的結果是...
摘要:每一個由構造函數創建的對象都會默認的連接到該神秘對象上。在構造方法中也具有類似的功能,因此也稱其為類實例與對象實例一般是指某一個構造函數創建出來的對象,我們稱為構造函數的實例實例就是對象。表示該原型是與什么構造函數聯系起來的。 本文您將看到以下內容: 傳統構造函數的問題 一些相關概念 認識原型 構造、原型、實例三角結構圖 對象的原型鏈 函數的構造函數Function 一句話說明什么...
摘要:一作用域域表示的就是范圍,即作用域,就是一個名字在什么地方可以使用,什么時候不能使用。概括的說作用域就是一套設計良好的規則來存儲變量,并且之后可以方便地找到這些變量。 一、作用域 域表示的就是范圍,即作用域,就是一個名字在什么地方可以使用,什么時候不能使用。想了解更多關于作用域的問題推薦閱讀《你不知道的JavaScript上卷》第一章(或第一部分),從編譯原理的角度說明什么是作用域。概...
閱讀 1147·2021-11-25 09:43
閱讀 2965·2019-08-30 15:54
閱讀 3349·2019-08-30 15:54
閱讀 2991·2019-08-30 15:44
閱讀 1623·2019-08-26 12:18
閱讀 2255·2019-08-26 11:42
閱讀 875·2019-08-26 11:35
閱讀 3295·2019-08-23 18:22