摘要:此時產(chǎn)生了閉包。導致,函數(shù)的活動對象沒有被銷毀。是不是跟你想的不一樣其實,這個例子重點就在函數(shù)上,這個函數(shù)的第一個參數(shù)接受一個函數(shù)作為回調(diào)函數(shù),這個回調(diào)函數(shù)并不會立即執(zhí)行,它會在當前代碼執(zhí)行完,并在給定的時間后執(zhí)行。
上一節(jié)說了執(zhí)行上下文,這節(jié)咱們就乘勝追擊來搞搞閉包!頭疼的東西讓你不再頭疼!
一、函數(shù)也是引用類型的。function f(){ console.log("not change") }; var ff = f; function f(){ console.log("changed") }; ff(); //"changed" //ff 保存著函數(shù) f 的引用,改變f 的值, ff也變了 //來個對比,估計你就明白了。 var f = "not change"; var ff = f; f = "changed"; console.log(ff); //"not change" //ff 保存著跟 f 一樣的值,改變f 的值, ff 不會變
其實,就是引用類型 和 基本類型的 區(qū)別。
function f(arg){ console.log(arg) } f(); //undefined function f(arg){ arg = 5; console.log(arg); } f(); //5
基本類型時,變量保存的是數(shù)據(jù),引用類型時,變量保存的是內(nèi)存地址。參數(shù)傳遞,就是把變量保存的值 復制給 參數(shù)。
var o = { a: 5 }; function f(arg){ arg.a = 6; } f(o); console.log(o.a); //6
JavaScript 具有自動垃圾收集機制,執(zhí)行環(huán)境會負責管理代碼執(zhí)行過程中使用的內(nèi)存。函數(shù)中,正常的局部變量和函數(shù)聲明只在函數(shù)執(zhí)行的過程中存在,當函數(shù)執(zhí)行結(jié)束后,就會釋放它們所占的內(nèi)存(銷毀變量和函數(shù))。
而js 中 主要有兩種收集方式:
標記清除(常見) //給變量標記為“進入環(huán)境” 和 “離開環(huán)境”,回收標記為“離開環(huán)境”的變量。
引用計數(shù) // 一個引用類型值,被賦值給一個變量,引用次數(shù)加1,通過變量取得引用類型值,則減1,回收為次數(shù)為0 的引用類型值。
知道個大概情況就可以了,《JavaScript高級程序設計 第三版》 4.3節(jié) 有詳解,有興趣,可以看下。.
之前說過,JavaScript中的作用域無非就是兩種:全局作用域和局部作用域。
根據(jù)作用域鏈的特性,我們知道,作用域鏈是單向的。也就是說,在函數(shù)內(nèi)部,可以直接訪問函數(shù)外部和全局變量,函數(shù)。但是,反過來,函數(shù)外部和全局,是訪問不了函數(shù)內(nèi)的變量,函數(shù)的。
function testA(){ var a = 666; } console.log(a); //報錯,a is not defined var b = 566; function testB(){ console.log(b); } //566
但是,有時候,我們需要在函數(shù)外部 訪問函數(shù)內(nèi)部的變量,函數(shù)。一般情況下,我們是辦不到的,這時,我們就需要閉包來實現(xiàn)了。
function fa(){ var va = "this is fa"; function fb(){ console.log(va); } return fb; } var fc = fa(); fc(); //"this is fa"
想要讀取fa 函數(shù)內(nèi)的變量 va,我們在內(nèi)部定義了一個函數(shù) fb,但是不執(zhí)行它,把它返回給外部,用 變量fc接受。此時,在外部再執(zhí)行fc,就讀取了fa 函數(shù)內(nèi)的變量 va。
其實,簡單點說,就是在 A 函數(shù)內(nèi)部,存在 B 函數(shù), B函數(shù) 在 A 函數(shù) 執(zhí)行完畢后再執(zhí)行。B執(zhí)行時,訪問了已經(jīng)執(zhí)行完畢的 A函數(shù)內(nèi)部的變量和函數(shù)。
由此可知:閉包是函數(shù)A的執(zhí)行環(huán)境 以及 執(zhí)行環(huán)境中的函數(shù) B組合而構(gòu)成的。
上篇文章中說過,變量等 都儲存在 其所在執(zhí)行環(huán)境的活動對象中,所以說是 函數(shù)A 的執(zhí)行環(huán)境。
當 函數(shù)A執(zhí)行完畢后,函數(shù)B再執(zhí)行,B的作用域中就保留著 函數(shù)A 的活動對象,因此B中可以訪問 A中的 變量,函數(shù),arguments對象。此時產(chǎn)生了閉包。大部分書中,都把 函數(shù)B 稱為閉包,而在谷歌瀏覽器中,把 A函數(shù)稱為閉包。
之前說過,當函數(shù)執(zhí)行完畢后,局部活動對象就會被銷毀。其中保存的變量,函數(shù)都會被銷毀。內(nèi)存中僅保存全局作用域(全局執(zhí)行環(huán)境的變量對象)。但是,閉包的情況就不同了。
以上面的例子來說,函數(shù)fb 和其所在的環(huán)境 函數(shù)fa,就組成了閉包。函數(shù)fa執(zhí)行完畢后,按道理說, 函數(shù)fa 執(zhí)行環(huán)境中的 活動對象就應該被銷毀了。但是,因為 函數(shù)fa 執(zhí)行時,其中的 函數(shù)fb 被 返回,被 變量fc 引用著。導致,函數(shù)fa 的活動對象沒有被銷毀。而在其后 fc() 執(zhí)行,就是 函數(shù)fb 執(zhí)行時,構(gòu)建的作用域中保存著 函數(shù)fa 的活動對象,因此,函數(shù)fb 中 可以通過作用域鏈訪問 函數(shù)fa 中的變量。
我已經(jīng)盡力地說明白了。就看各位的了。哈哈!其實,簡單的說:就是fa函數(shù)執(zhí)行完畢了,其內(nèi)部的 fb函數(shù)沒有執(zhí)行,并返回fb的引用,當fb再次執(zhí)行時,fb的作用域中保留著 fa函數(shù)的活動對象。
再來個有趣經(jīng)典的例子:
for (var i=1; i<=5; i++) { setTimeout(function(){ console.log(i); },i*1000); } //每隔一秒輸出一個6,共5個。
是不是跟你想的不一樣?其實,這個例子重點就在setTimeout函數(shù)上,這個函數(shù)的第一個參數(shù)接受一個函數(shù)作為回調(diào)函數(shù),這個回調(diào)函數(shù)并不會立即執(zhí)行,它會在當前代碼執(zhí)行完,并在給定的時間后執(zhí)行。這樣就導致了上面情況的發(fā)生。
可以下面對這個例子進行變形,可以有助于你的理解把:
var i = 1; while(i <= 5){ setTimeout(function(){ console.log(i); },i*1000) i = i+1; }
正因為,setTimeout里的第一個函數(shù)不會立即執(zhí)行,當這段代碼執(zhí)行完之后,i 已經(jīng) 被賦值為6了(等于5時,進入循環(huán),最后又加了1),所以 這時再執(zhí)行setTimeout 的回調(diào)函數(shù),讀取 i 的值,回調(diào)函數(shù)作用域內(nèi)沒有i,向上讀取,上面作用域內(nèi)i的值就是6了。但是 i * 1000,是立即執(zhí)行的,所以,每次讀的 i 值 都是對的。
這時候,就需要利用閉包來保存每個循環(huán)時, i 不同的值。
function makeClosures(i){ //這里就和 內(nèi)部的匿名函數(shù)構(gòu)成閉包了 var i = i; //這步是不需要的,為了讓看客們看的輕松點 return function(){ console.log(i); //匿名沒有執(zhí)行,它可以訪問i 的值,保存著這個i 的值。 } } for (var i=1; i<=5; i++) { setTimeout(makeClosures(i),i*1000); //這里簡單說下,這里makeClosures(i), 是函數(shù)執(zhí)行,并不是傳參,不是一個概念 //每次循環(huán)時,都執(zhí)行了makeClosures函數(shù),都返回了一個沒有被執(zhí)行的匿名函數(shù) //(這里就是返回了5個匿名函數(shù)),每個匿名函數(shù)都是一個局部作用域,保存著每次傳進來的i值 //因此,每個匿名函數(shù)執(zhí)行時,讀取`i`值,都是自己作用域內(nèi)保存的值,是不一樣的。所以,就得到了想要的結(jié)果 } //1 //2 //3 //4 //5
閉包的關鍵就在,外部的函數(shù)執(zhí)行完畢后,內(nèi)部的函數(shù)再執(zhí)行,并訪問了外部函數(shù)內(nèi)的變量。
你可能在別處,或者自己想到了下面這種解法:
for (var i=1; i<=5; i++) { (function(i){ setTimeout(function(){ console.log(i); },i*1000); })(i); }
如果你一直把這個當做閉包,那你可能看到的是不同的閉包定義吧(犀牛書和高程對閉包的定義不同)。嚴格來說,這不是閉包,這是利用了立即執(zhí)行函數(shù) 和 函數(shù)作用域 來解決的。
做下變形,你再看看:
for (var i=1; i<=5; i++) { function f(i){ setTimeout(function(){ console.log(i); },i*1000); }; f(i); }
這樣看就很明顯了吧,主要是利用了函數(shù)作用域,而使用立即執(zhí)行函數(shù),是為了簡化步驟。
總結(jié):判斷是不是閉包,我總結(jié)了要滿足以下三點:
兩個函數(shù)。有內(nèi)函數(shù) 和 外函數(shù)。
外函數(shù)執(zhí)行完畢后,內(nèi)函數(shù) 還沒有執(zhí)行。
當內(nèi)函數(shù)執(zhí)行時(通過外部引用或者返回內(nèi)函數(shù)),訪問了 外函數(shù)內(nèi)部的 變量,函數(shù)等(說是訪問,其實內(nèi)函數(shù)保存著外函數(shù)的活動對象,因此,arguments對象也可以訪問到)。
其實這道題,知道ES6的 let 關鍵詞,估計也想到了另一個解法:
for (let i=1; i<=5; i++) { //這里的關鍵就是使用的let 關鍵詞,來形成塊級作用域 setTimeout(function(){ console.log(i); },i*1000); }
我不知道,大家有沒有疑惑啊,為啥使用了塊級作用域就可以了呢。反正我當初就糾結(jié)了半天。
11月 2日修正:
這個答案的關鍵就在于 塊級作用域的規(guī)則了。它讓let 聲明的變量只在{}內(nèi)有效,外部是訪問不了的。
做下變形:
for (var i=1; i<=5; i++) { let j = i; setTimeout(function(){ console.log(j); },j*1000); }
其實,for 循環(huán)時,每次都會用let 或者 var 創(chuàng)建一個新變量,并以之前迭代中同名變量的值將其初始化。而這里正因為使用let,導致每次循環(huán)都會創(chuàng)建一個新的塊級作用域,這樣,雖然setTimeout 中的匿名函數(shù)內(nèi)沒有 i 值,但它向上作用域讀取i 值,就讀到了塊級作用域內(nèi) i 的值。
上面用立即執(zhí)行函數(shù)模擬塊級作用域,就是這個道理啦!
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/96109.html
摘要:為了更好的理解,在閱讀此文之前建議先閱讀上一篇進擊之詞法作用域與作用域鏈什么是閉包閉包的含義就是閉合,包起來,簡單的來說,就是一個具有封閉功能與包裹功能的結(jié)構(gòu)。在中函數(shù)構(gòu)成閉包。 為了更好的理解,在閱讀此文之前建議先閱讀上一篇《進擊JavaScript之詞法作用域與作用域鏈》 1.什么是閉包 閉包的含義就是閉合,包起來,簡單的來說,就是一個具有封閉功能與包裹功能的結(jié)構(gòu)。所謂的閉包就是...
摘要:匿名函數(shù)是不能單獨寫的,所以就提不上立即執(zhí)行了。六立即執(zhí)行函數(shù)在閉包中的應用立即執(zhí)行函數(shù)能配合閉包保存狀態(tài)。來看下上節(jié)內(nèi)容中閉包的例子現(xiàn)在,我們來利用立即執(zhí)行函數(shù)來簡化它第一個匿名函數(shù)執(zhí)行完畢后,返回了第二個匿名函數(shù)。 前面的閉包中,提到與閉包相似的立即執(zhí)行函數(shù),感覺兩者還是比較容易弄混吧,嚴格來說(因為犀牛書和高程對閉包的定義不同),立即執(zhí)行函數(shù)并不屬于閉包,它不滿足閉包的三個條件。...
摘要:如下代碼輸出的結(jié)果是代碼執(zhí)行分為兩個大步預解析的過程代碼的執(zhí)行過程預解析與變量聲明提升程序在執(zhí)行過程中,會先將代碼讀取到內(nèi)存中檢查,會將所有的聲明在此進行標記,所謂的標記就是讓解析器知道有這個名字,后面在使用名字的時候不會出現(xiàn)未定義的錯誤。 showImg(https://segmentfault.com/img/remote/1460000012922850); 如下代碼輸出的結(jié)果是...
摘要:每一個由構(gòu)造函數(shù)創(chuàng)建的對象都會默認的連接到該神秘對象上。在構(gòu)造方法中也具有類似的功能,因此也稱其為類實例與對象實例一般是指某一個構(gòu)造函數(shù)創(chuàng)建出來的對象,我們稱為構(gòu)造函數(shù)的實例實例就是對象。表示該原型是與什么構(gòu)造函數(shù)聯(lián)系起來的。 本文您將看到以下內(nèi)容: 傳統(tǒng)構(gòu)造函數(shù)的問題 一些相關概念 認識原型 構(gòu)造、原型、實例三角結(jié)構(gòu)圖 對象的原型鏈 函數(shù)的構(gòu)造函數(shù)Function 一句話說明什么...
摘要:一作用域域表示的就是范圍,即作用域,就是一個名字在什么地方可以使用,什么時候不能使用。概括的說作用域就是一套設計良好的規(guī)則來存儲變量,并且之后可以方便地找到這些變量。 一、作用域 域表示的就是范圍,即作用域,就是一個名字在什么地方可以使用,什么時候不能使用。想了解更多關于作用域的問題推薦閱讀《你不知道的JavaScript上卷》第一章(或第一部分),從編譯原理的角度說明什么是作用域。概...
閱讀 1179·2021-11-25 09:43
閱讀 3004·2019-08-30 15:54
閱讀 3377·2019-08-30 15:54
閱讀 3032·2019-08-30 15:44
閱讀 1674·2019-08-26 12:18
閱讀 2279·2019-08-26 11:42
閱讀 897·2019-08-26 11:35
閱讀 3316·2019-08-23 18:22