閉包

相信眾多JS的lovers都聽(tīng)說(shuō)過(guò)這句話:閉包很重要但是很難理解。

先看一個(gè)閉包的例子:

function foo() {

? ? let a = 2;

? ? function bar() {

? ? ? ? console.log( a );

? ? }

? ? return bar;

}

let baz = foo();

baz();

大家肯定都寫過(guò)類似的代碼,相信很多小伙伴也知道這段代碼應(yīng)用了閉包,我們慢慢分析:

首先必須先知道閉包是什么,才能分析出閉包為什么產(chǎn)生和閉包到底在哪?

當(dāng)一個(gè)函數(shù)能夠記住并訪問(wèn)到其所在的詞法作用域及作用域鏈,特別強(qiáng)調(diào)是在其定義的作用域外進(jìn)行的訪問(wèn),此時(shí)該函數(shù)和其上層執(zhí)行上下文共同構(gòu)成閉包。

需要明確的幾點(diǎn):

閉包一定是函數(shù)對(duì)象(wintercn大大的閉包考證)

閉包和詞法作用域,作用域鏈,垃圾回收機(jī)制息息相關(guān)

當(dāng)函數(shù)一定是在其定義的作用域外進(jìn)行的訪問(wèn)時(shí),才產(chǎn)生閉包

閉包是由該函數(shù)和其上層執(zhí)行上下文共同構(gòu)成(這點(diǎn)稍后我會(huì)說(shuō)明)

閉包是什么,我們說(shuō)清楚了,下面我們看下閉包是如何產(chǎn)生的。

接下來(lái),我默認(rèn)你已經(jīng)讀過(guò)我之前的兩篇文章 原來(lái)JavaScript內(nèi)部是這樣運(yùn)行的 和 徹底搞懂JavaScript作用域 , 建議先進(jìn)行閱讀理解JS執(zhí)行機(jī)制和作用域等相關(guān)知識(shí),再理解閉包,否則可能會(huì)理解的不透徹。

現(xiàn)在我假設(shè)JS引擎執(zhí)行到這行代碼

let baz = foo();

這個(gè)時(shí)候foo函數(shù)已經(jīng)執(zhí)行完,JS的垃圾回收機(jī)制應(yīng)該會(huì)自動(dòng)將其標(biāo)記為"離開(kāi)環(huán)境",等待回收機(jī)制下次執(zhí)行,將其內(nèi)存進(jìn)行釋放(標(biāo)記清除)。

但是,我們仔細(xì)看圖中粉色的箭頭,我們將bar的引用指向baz,正是這種引用賦值,阻止了垃圾回收機(jī)制將foo進(jìn)行回收,從而導(dǎo)致bar的整條作用域鏈都被保存下來(lái)。

接下來(lái),baz()執(zhí)行,bar進(jìn)入執(zhí)行棧,閉包(foo)形成,此時(shí)bar中依舊可以訪問(wèn)到其父作用域氣泡中的變量a。

這樣說(shuō)可能不是很清晰,接下來(lái)我們借助chrome的調(diào)試工具看下閉包產(chǎn)生的過(guò)程。

當(dāng)JS引擎執(zhí)行到這行代碼let baz = foo();時(shí):let baz = foo();已經(jīng)執(zhí)行完,即將執(zhí)行baz();,此時(shí)Call Stack中只有全局上下文。

接下來(lái)baz();執(zhí)行:

此時(shí)bar進(jìn)入Call Stack中,并且Closure(foo)形成。

針對(duì)上面我提到的幾點(diǎn)進(jìn)行下說(shuō)明:

上述第二點(diǎn)(閉包和詞法作用域,作用域鏈,垃圾回收機(jī)制息息相關(guān))大家應(yīng)該都清楚了

上述第三點(diǎn),當(dāng)函數(shù)baz執(zhí)行時(shí),閉包才生成

上述第四點(diǎn),閉包是foo,并不是bar,很多書(《you dont know JavaScript》《JavaScript高級(jí)程序設(shè)計(jì)》)中,都強(qiáng)調(diào)保存下來(lái)的引用,即上例中的bar是閉包,而chrome認(rèn)為被保存下來(lái)的封閉空間foo是閉包,針對(duì)這點(diǎn)我贊同chrome的判斷(僅為自己的理解,如有不同意見(jiàn),歡迎來(lái)討論)

閉包不是很神秘,反而是在我們的程序中隨處可見(jiàn),當(dāng)我們靜下心來(lái),品味閉包的味道,發(fā)現(xiàn)它散發(fā)出一種藝術(shù)的美,樸實(shí)、精巧又不失優(yōu)雅。

細(xì)想,在我們作用域氣泡模型中,作用域鏈讓我們的內(nèi)部bar氣泡能夠"看到"外面的世界,而閉包則讓我們的外部作用域能夠"關(guān)注到"內(nèi)部的情況成為可能??梢?jiàn),只要我們?cè)敢猓瑑?nèi)心世界和外面世界是可以相通的。

閉包的應(yīng)用的注意事項(xiàng)

閉包,在JS中絕對(duì)是一個(gè)高貴的存在,它讓很多不可能實(shí)現(xiàn)的代碼成為可能,但是物雖好,也要合理使用,不然不但不能達(dá)到我們想要的效果,有的時(shí)候可能還會(huì)適得其反。

內(nèi)存泄漏(Memory Leak)

JavaScript分配給Web瀏覽器的可用內(nèi)存數(shù)量通常比分配給桌面應(yīng)用程序的少,這樣做主要是防止JavaScript的網(wǎng)頁(yè)耗盡全部系統(tǒng)內(nèi)存而導(dǎo)致系統(tǒng)崩潰。

因此,要想使頁(yè)面具有更好的性能,就必須確保頁(yè)面占用最少的內(nèi)存資源,也就是說(shuō),我們應(yīng)該保證執(zhí)行代碼只保存有用的數(shù)據(jù),一旦數(shù)據(jù)不再有用,我們就應(yīng)該讓垃圾回收機(jī)制對(duì)其進(jìn)行回收,釋放內(nèi)存。

我們現(xiàn)在都知道了閉包阻止了垃圾回收機(jī)制對(duì)變量進(jìn)行回收,因此變量會(huì)永遠(yuǎn)存在內(nèi)存中,即使當(dāng)變量不再被使用時(shí),這樣會(huì)造成內(nèi)存泄漏,會(huì)嚴(yán)重影響頁(yè)面的性能。因此當(dāng)變量對(duì)象不再適用時(shí),我們要將其釋放。

我們拿上面代碼舉例:

function foo() {

? ? let a = 2;

? ? function bar() {

? ? ? ? console.log( a );

? ? }

? ? return bar;

}

let baz = foo();

baz(); //baz指向的對(duì)象會(huì)永遠(yuǎn)存在堆內(nèi)存中

baz = null; //如果baz不再使用,將其指向的對(duì)象釋放

關(guān)于內(nèi)存泄漏,推薦 阮一峰老師博客。

閉包的應(yīng)用

1.模塊

一個(gè)模塊應(yīng)該具有私有屬性、私有方法和公有屬性、公有方法。

而閉包能很好的將模塊的公有屬性、方法暴露出來(lái)。

var myModule = (function (window, undefined) {

let name = "echo";

function getName() {

return name;

}

return {

name,

getName

}

})(window);

console.log( myModule.name ); // echo

console.log( myModule.getName() ); // echo

"return"關(guān)鍵字將對(duì)象引用導(dǎo)出賦值給myModule,從而應(yīng)用到閉包。

2.延時(shí)器(setTimeout)、計(jì)數(shù)器(setInterval)

這里簡(jiǎn)單寫一個(gè)常見(jiàn)的關(guān)于閉包的面試題。

for( var i = 0; i < 5; i++ ) {

setTimeout(() => {

console.log( i );

}, 1000 * i)

}

答案大家都知道:每秒鐘輸出一個(gè)5,一共輸出5次。

那么如何做到每秒鐘輸出一個(gè)數(shù),以此為0,1,2,3,4呢?

我們這里只介紹閉包的解決方法,其他類似塊作用域等等的解決方法,我們這里不討論。

for( var i = 0; i < 5; i++ ) {

((j) => {

setTimeout(() => {

console.log( j );

}, 1000 * j)

})(i)

}

"setTimeout"方法里應(yīng)用了閉包,使其內(nèi)部能夠記住每次循環(huán)所在的詞法作用域和作用域鏈。

由于setTimeout中的回調(diào)函數(shù)會(huì)在當(dāng)前任務(wù)隊(duì)列的尾部進(jìn)行執(zhí)行,因此上面第一個(gè)例子中每次循環(huán)中的setTimeout回調(diào)函數(shù)記住的i的值是for循環(huán)作用域中的值,此時(shí)都是5,而第二個(gè)例子記住的i的數(shù)為setTimeout的父級(jí)作用域自執(zhí)行函數(shù)中的j的值,依次為0,1,2,3,4。

3.監(jiān)聽(tīng)器

var oDiv = document.querySeletor("#div");

oDiv.addEventListener('click', function() {

console.log( oDiv.id );

})

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 寫這篇文章時(shí)的心情是十分忐忑的,因?yàn)閷?duì)于我們今天的主角:閉包,很多小伙伴都寫過(guò)關(guān)于它的文章,相信大家也讀過(guò)不少,那...
    前端彭于晏閱讀 367評(píng)論 0 0
  • 官方中文版原文鏈接 感謝社區(qū)中各位的大力支持,譯者再次奉上一點(diǎn)點(diǎn)福利:阿里云產(chǎn)品券,享受所有官網(wǎng)優(yōu)惠,并抽取幸運(yùn)大...
    HetfieldJoe閱讀 5,717評(píng)論 16 88
  • 特別說(shuō)明,為便于查閱,文章轉(zhuǎn)自https://github.com/getify/You-Dont-Know-JS...
    殺破狼real閱讀 563評(píng)論 0 0
  • 閉包,是真的美 寫這篇文章時(shí)的心情是十分忐忑的,因?yàn)閷?duì)于我們今天的主角:閉包,很多小伙伴都寫過(guò)關(guān)于它的文章,相信大...
    對(duì)方正在輸入_zs閱讀 242評(píng)論 0 1
  • BY 張建成(prettyEcho@github)除非另行注明,頁(yè)面上所有內(nèi)容采用知識(shí)共享-署名(CC BY 2....
    echo_me閱讀 347評(píng)論 0 0

友情鏈接更多精彩內(nèi)容