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