摘要:由此可知閉包是函數(shù)的執(zhí)行環(huán)境以及執(zhí)行環(huán)境中的函數(shù)組合而構(gòu)成的。此時(shí)產(chǎn)生了閉包。二閉包的作用閉包的特點(diǎn)是讀取函數(shù)內(nèi)部局部變量,并將局部變量保存在內(nèi)存,延長其生命周期。三閉包的問題使用閉包會(huì)將局部變量保持在內(nèi)存中,所以會(huì)占用大量內(nèi)存,影響性能。
一、什么是閉包 1.閉包的定義
閉包是一種特殊的對象。它由兩部分構(gòu)成:函數(shù),以及創(chuàng)建該函數(shù)的環(huán)境(包含自由變量)。環(huán)境由閉包創(chuàng)建時(shí)在作用域中的任何局部變量組成。
閉包是指有權(quán)訪問另外一個(gè)函數(shù)作用域中的變量的函數(shù)
閉包是函數(shù)以及函數(shù)聲明所在的詞法環(huán)境的組合。
由此,我們可以看出閉包共有兩部分組成:閉包 = 函數(shù) + 函數(shù)能夠訪問的自由變量
舉個(gè)例子:
var a = 1; function foo() { console.log(a); } foo();
foo 函數(shù)可以訪問變量 a,但是 a 既不是 foo 函數(shù)的局部變量,也不是 foo 函數(shù)的參數(shù),所以 a 就是自由變量。那么,函數(shù) foo + foo 函數(shù)訪問的自由變量 a 不就是構(gòu)成了一個(gè)閉包嘛
2.閉包的概念function fa(){ var va = "this is fa"; function fb(){ console.log(va); } return fb; } var fc = fa(); fc(); //"this is fa"
其實(shí),簡單點(diǎn)說,就是在 A 函數(shù)內(nèi)部,存在 B 函數(shù), B函數(shù) 在 A 函數(shù) 執(zhí)行完畢后再執(zhí)行。B執(zhí)行時(shí),訪問了已經(jīng)執(zhí)行完畢的 A函數(shù)內(nèi)部的變量和函數(shù)。
由此可知:閉包是函數(shù)A的執(zhí)行環(huán)境以及執(zhí)行環(huán)境中的函數(shù)B組合而構(gòu)成的。
變量都儲(chǔ)存在其所在執(zhí)行環(huán)境的活動(dòng)對象中,所以說是函數(shù)A的執(zhí)行環(huán)境。
當(dāng)函數(shù)A執(zhí)行完畢后,函數(shù)B再執(zhí)行,B的作用域中就保留著函數(shù)A的活動(dòng)對象,因此B中可以訪問A中的變量,函數(shù),arguments對象。此時(shí)產(chǎn)生了閉包。大部分書中,都把函數(shù)B稱為閉包,而在谷歌瀏覽器中,把A函數(shù)稱為閉包。
3.閉包的本質(zhì)之前說過,當(dāng)函數(shù)執(zhí)行完畢后,局部活動(dòng)對象就會(huì)被銷毀。其中保存的變量,函數(shù)都會(huì)被銷毀。內(nèi)存中僅保存全局作用域(全局執(zhí)行環(huán)境的變量對象)。但是,閉包的情況就不同了。
以上面的例子來說,函數(shù)fb和其所在的環(huán)境函數(shù)fa,就組成了閉包。函數(shù)fa執(zhí)行完畢后,按道理說, 函數(shù)fa執(zhí)行環(huán)境中的 活動(dòng)對象就應(yīng)該被銷毀了。但是,因?yàn)?b>函數(shù)fa執(zhí)行時(shí),其中的函數(shù)fb被返回,被變量fc引用著。導(dǎo)致,函數(shù)fa的活動(dòng)對象沒有被銷毀。而在其后fc()執(zhí)行,就是函數(shù)fb執(zhí)行時(shí),構(gòu)建的作用域中保存著函數(shù)fa的活動(dòng)對象,因此,函數(shù)fb中可以通過作用域鏈訪問函數(shù)fa中的變量。
其實(shí),簡單的說:就是fa函數(shù)執(zhí)行完畢了,其內(nèi)部的fb函數(shù)沒有執(zhí)行,并返回fb的引用,當(dāng)fb再次執(zhí)行時(shí),fb的作用域中保留著fa函數(shù)的活動(dòng)對象。
二、閉包的作用閉包的特點(diǎn)是讀取函數(shù)內(nèi)部局部變量,并將局部變量保存在內(nèi)存,延長其生命周期。
作用
通過閉包,在外部環(huán)境訪問內(nèi)部環(huán)境的變量。
使得這些變量一直保存在內(nèi)存中,不會(huì)被垃圾回收。
以使用閉包實(shí)現(xiàn)以下功能:
1.解決類似循環(huán)綁定事件的問題在實(shí)際開發(fā)中,經(jīng)常會(huì)遇到需要循環(huán)綁定事件的需求,比如在id為container的元素中添加5個(gè)按鈕,每個(gè)按鈕的文案是相應(yīng)序號,點(diǎn)擊打印輸出對應(yīng)序號。
其中第一個(gè)方法很容易錯(cuò)誤寫成:
var container = document.getElementById("container"); for(var i = 1; i <= 5; i++) { var btn = document.createElement("button"), text = document.createTextNode(i); btn.appendChild(text); btn.addEventListener("click", function(){ console.log(i); }) container.appendChild(btn); }
雖然給不同的按鈕分別綁定了事件函數(shù),但是5個(gè)函數(shù)其實(shí)共享了一個(gè)變量 i。由于點(diǎn)擊事件在 js 代碼執(zhí)行完成之后發(fā)生,此時(shí)的變量 i 值為6,所以每個(gè)按鈕點(diǎn)擊打印輸出都是6。
為了解決這個(gè)問題,我們可以修改代碼,給各個(gè)點(diǎn)擊事件函數(shù)建立獨(dú)立的閉包,保持不同狀態(tài)的i。
var container = document.getElementById("container"); for(var i = 1; i <= 5; i++) { (function(_i) { var btn = document.createElement("button"), text = document.createTextNode(_i); btn.appendChild(text); btn.addEventListener("click", function(){ console.log(_i); }) container.appendChild(btn); })(i); }
注:解決這個(gè)問題更好的方法是使用 ES6 的 let,聲明塊級的局部變量。2.封裝私有變量 (1) 經(jīng)典的計(jì)數(shù)器例子:
function makeCounter() { var value = 0; return { getValue: function() { return value; }, increment: function() { value++; }, decrement: function() { value--; } } } var a = makeCounter(); var b = makeCounter(); b.increment(); b.increment(); b.decrement(); b.getValue(); // 1 a.getValue(); // 0 a.value; // undefined
每次調(diào)用makeCounter函數(shù),環(huán)境是不相同的,所以對b進(jìn)行的increment/decrement操作不會(huì)影響a的value屬性。同時(shí),對value屬性,只能通過getValue方法進(jìn)行訪問,而不能直接通過value屬性進(jìn)行訪問。
(2) 經(jīng)典的循環(huán)閉包面試題for (var i=1;i<=5;i++){ setTimeout(function timer(){ console.log(i); },i*1000); }
正常預(yù)想下,上面這段代碼我們以為是分別輸出數(shù)字1-5,每秒一個(gè)。
但實(shí)際上,運(yùn)行時(shí)輸出的卻是每秒輸出一個(gè)6,一共五次。
Why?
for循環(huán)有一個(gè)特點(diǎn),就是“i判斷失敗一次才停止”。所以,i在不斷的自加1的時(shí)候,直到i等于5,i才失敗,這時(shí)候循環(huán)體不再執(zhí)行,會(huì)跳出,所以i等于5沒錯(cuò)。那么為什么5次循環(huán)的i都等于5?原因就是setTimeout()的回調(diào),也就是console.log(i);被壓到任務(wù)隊(duì)列的最后,for循環(huán)是同步任務(wù),所以先執(zhí)行,等于是空跑了5次循環(huán)。于是,i都等于5之后,console.log(i);剛開始第一次執(zhí)行,當(dāng)然輸出全是5。
根據(jù)setTimeout定義的操作在函數(shù)調(diào)用棧清空之后才會(huì)執(zhí)行的特點(diǎn),for循環(huán)里定義了5個(gè)setTimeout操作。而當(dāng)這些操作開始執(zhí)行時(shí),for循環(huán)的i值,已經(jīng)先一步變成了6。因此輸出結(jié)果總為6。而我們想要讓輸出結(jié)果依次執(zhí)行,我們就必須借助閉包的特性,每次循環(huán)時(shí),將i值保存在一個(gè)閉包中,當(dāng)setTimeout中定義的操作執(zhí)行時(shí),則訪問對應(yīng)閉包保存的i值即可。
簡單來說,原因是,延遲函數(shù)的回調(diào)會(huì)在循環(huán)結(jié)束時(shí)才執(zhí)行。
根據(jù)作用域的工作原理,循環(huán)中的五個(gè)函數(shù)是在各個(gè)迭代中分別定義的,但是它們都被封閉在一個(gè)共享的全局作用域中,實(shí)際上只有一個(gè)i。
解決辦法
利用立即執(zhí)行函數(shù)和函數(shù)作用域來解決,用自執(zhí)行函數(shù)傳參,這樣自執(zhí)行函數(shù)內(nèi)部形成了局部作用域,不受外部變量變化的影響。
我們可以通過立即執(zhí)行函數(shù)創(chuàng)建作用域。(立即執(zhí)行函數(shù)會(huì)通過聲明并立即執(zhí)行一個(gè)函數(shù)來創(chuàng)建作用域)。
for (var i=1; i<=5; i++) { (function(i) { setTimeout( function timer() { console.log(i); }, i*1000 ); })(i) } // 1 // 2 // 3 // 4 // 5
function makeClosures(i){ //這里就和 內(nèi)部的匿名函數(shù)構(gòu)成閉包了 var i = i; //這步是不需要的,為了讓看客們看的輕松點(diǎn) return function(){ console.log(i); //匿名沒有執(zhí)行,它可以訪問i的值,保存著這個(gè)i的值。 } } for (var i=1; i<=5; i++) { setTimeout(makeClosures(i),i*1000); //這里簡單說下,這里makeClosures(i), 是函數(shù)執(zhí)行,并不是傳參,不是一個(gè)概念 //每次循環(huán)時(shí),都執(zhí)行了makeClosures函數(shù),都返回了一個(gè)沒有被執(zhí)行的匿名函數(shù) //(這里就是返回了5個(gè)匿名函數(shù)),每個(gè)匿名函數(shù)都是一個(gè)局部作用域,保存著每次傳進(jìn)來的i值 //因此,每個(gè)匿名函數(shù)執(zhí)行時(shí),讀取`i`值,都是自己作用域內(nèi)保存的值,是不一樣的。所以,就得到了想要的結(jié)果 } //1 //2 //3 //4 //5
ES6引入的let在循環(huán)中不止會(huì)被聲明一次,在每次迭代都會(huì)聲明:
for (let i=1;i<=5;i++){ setTimeout(function timer(){ console.log(i); },i*1000); }
因?yàn)槭褂胠et,導(dǎo)致每次循環(huán)都會(huì)創(chuàng)建一個(gè)新的塊級作用域,這樣,雖然setTimeout 中的匿名函數(shù)內(nèi)沒有 i 值,但它向上作用域讀取i 值,就讀到了塊級作用域內(nèi) i 的值。
三、閉包的問題使用閉包會(huì)將局部變量保持在內(nèi)存中,所以會(huì)占用大量內(nèi)存,影響性能。所以在不再需要使用這些局部變量的時(shí)候,應(yīng)該手動(dòng)將這些變量設(shè)置為null, 使變量能被回收。
當(dāng)閉包的作用域中保存一些DOM節(jié)點(diǎn)時(shí),較容易出現(xiàn)循環(huán)引用,可能會(huì)造成內(nèi)存泄漏。原因是在IE9以下的瀏覽器中,由于BOM 和DOM中的對象是使用C++以COM 對象的方式實(shí)現(xiàn)的,而COM對象的垃圾收集機(jī)制采用的是引用計(jì)數(shù)策略,當(dāng)出現(xiàn)循環(huán)引用時(shí),會(huì)導(dǎo)致對象無法被回收。當(dāng)然,同樣可以通過設(shè)置變量為null解決。
舉例如下:
function func() { var element = document.getElementById("test"); element.onClick = function() { console.log(element.id); }; }
func 函數(shù)為 element 添加了閉包點(diǎn)擊事件,匿名函數(shù)中又對element進(jìn)行了引用,使得 element 的引用始終不為0。解決辦法是使用變量保存所需內(nèi)容,并在退出函數(shù)時(shí)將 element 置為 null。
function func() { var element = document.getElementById("test"), id = element.id; element.onClick = function() { console.log(id); }; element = null; }四、應(yīng)用場景:模塊與柯里化
模塊也是利用了閉包的一個(gè)強(qiáng)大的代碼模式。
function CoolModule(){ var something="cool"; var anothor=[1,2,3]; function doSomething(){ console.log(something); } function doAnthor(){ console.log(anothor.join("!")); } return{ doSomethig:doSomething, doAnothor:doAnother }; } var foo=CoolMOdule(); foo.doSomething();//cool foo.doAnother();//1!2!3
模塊有2個(gè)主要特征:
為創(chuàng)建內(nèi)部作用域而調(diào)用了一個(gè)包裝函數(shù);
包裝函數(shù)的返回值必須至少包括一個(gè)對內(nèi)部函數(shù)的引用,這樣就會(huì)創(chuàng)建涵蓋整個(gè)包裝函數(shù)內(nèi)部作用域的閉包。
import可以將一個(gè)模塊中的一個(gè)或多個(gè)API導(dǎo)入到當(dāng)前作用域中,并分別綁定在一個(gè)變量上;
module會(huì)將整個(gè)模塊的API導(dǎo)入并綁定到一個(gè)變量上;
export會(huì)將當(dāng)前模塊的一個(gè)標(biāo)識符導(dǎo)出為公共API。
如果你覺得這篇文章對你有所幫助,那就順便點(diǎn)個(gè)贊吧,點(diǎn)點(diǎn)關(guān)注不迷路~
黑芝麻哇,白芝麻發(fā),黑芝麻白芝麻哇發(fā)哈!
前端哇發(fā)哈
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/103002.html
摘要:在中,通過棧的存取方式來管理執(zhí)行上下文,我們可稱其為執(zhí)行棧,或函數(shù)調(diào)用棧。因?yàn)閳?zhí)行中最先進(jìn)入全局環(huán)境,所以處于棧底的永遠(yuǎn)是全局環(huán)境的執(zhí)行上下文。 一、什么是執(zhí)行上下文? 執(zhí)行上下文(Execution Context): 函數(shù)執(zhí)行前進(jìn)行的準(zhǔn)備工作(也稱執(zhí)行上下文環(huán)境) JavaScript在執(zhí)行一個(gè)代碼段之前,即解析(預(yù)處理)階段,會(huì)先進(jìn)行一些準(zhǔn)備工作,例如掃描JS中var定義的變量、...
摘要:腳本執(zhí)行,事件處理等。引擎線程,也稱為內(nèi)核,負(fù)責(zé)處理腳本程序,例如引擎。事件觸發(fā)線程,用來控制事件循環(huán)可以理解為,引擎線程自己都忙不過來,需要瀏覽器另開線程協(xié)助。異步請求線程,也就是發(fā)出請求后,接收響應(yīng)檢測狀態(tài)變更等都是這個(gè)線程管理的。 一、進(jìn)程與線程 現(xiàn)代操作系統(tǒng)比如Mac OS X,UNIX,Linux,Windows等,都是支持多任務(wù)的操作系統(tǒng)。 什么叫多任務(wù)呢?簡單地說,就是操...
摘要:令人困惑的是,文檔中稱,指定的回調(diào)函數(shù),總是排在前面。另外,由于指定的回調(diào)函數(shù)是在本次事件循環(huán)觸發(fā),而指定的是在下次事件循環(huán)觸發(fā),所以很顯然,前者總是比后者發(fā)生得早,而且執(zhí)行效率也高因?yàn)椴挥脵z查任務(wù)隊(duì)列。 一、定時(shí)器 除了放置異步任務(wù)的事件,任務(wù)隊(duì)列還可以放置定時(shí)事件,即指定某些代碼在多少時(shí)間之后執(zhí)行。這叫做定時(shí)器(timer)功能,也就是定時(shí)執(zhí)行的代碼。 定時(shí)器功能主要由setTim...
摘要:全局作用域局部作用域局部作用域全局作用域局部作用域塊語句沒有塊級作用域塊級聲明包括和,以及和循環(huán),和函數(shù)不同,它們不會(huì)創(chuàng)建新的作用域。局部作用域只在該函數(shù)調(diào)用執(zhí)行期間存在。 一、什么是作用域? 作用域是你的代碼在運(yùn)行時(shí),各個(gè)變量、函數(shù)和對象的可訪問性。(可產(chǎn)生作用的區(qū)域) 二、JavaScript中的作用域 在 JavaScript 中有兩種作用域 全局作用域 局部作用域 當(dāng)變量定...
摘要:許多程序設(shè)計(jì)語言都支持利用正則表達(dá)式進(jìn)行字符串操作。為字符串定義規(guī)則,為輸入內(nèi)容定義規(guī)則正則表達(dá)式用于字符串處理表單驗(yàn)證等場合,實(shí)用高效。匹配檢查字符串是否符合正則表達(dá)式中的規(guī)則,有一次不匹配,則返回。 一、正則表達(dá)式的定義 正則表達(dá)式(Regular Expression,在代碼中常簡寫為regex、regexp或RE)是計(jì)算機(jī)科學(xué)的一個(gè)概念。正則表達(dá)式使用單個(gè)字符串來描述、匹配一系...
閱讀 25659·2021-09-29 09:41
閱讀 4818·2021-09-10 11:20
閱讀 1936·2021-09-09 09:32
閱讀 1900·2019-08-30 15:44
閱讀 3209·2019-08-29 17:13
閱讀 2819·2019-08-29 14:14
閱讀 2076·2019-08-29 14:11
閱讀 3238·2019-08-29 12:36