您好,登錄后才能下訂單哦!
這篇“JavaScript閉包如何理解”文章的知識點大部分人都不太理解,所以小編給大家總結了以下內容,內容詳細,步驟清晰,具有一定的借鑒價值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來看看這篇“JavaScript閉包如何理解”文章吧。
閉包概念:
函數執?后返回結果是?個內部函數,并被外部變量所引?,如果內部函數持有被執?函數作?域的變量,即形成了閉包。可以在內部函數訪問到外部函數作?域。
使?閉包,?可以讀取函數中的變量,?可以將函數中的變量存儲在內存 中,保護變量不被污染。?正因閉包會把函數中的變量值存儲在內存中,會對內存有消耗,所以不能濫?閉包,否則會影響??性能,造成內存泄漏。當不需要使?閉包時,要及時釋放內存,可將內層函數對象的變量賦值為null。
閉包特點:一個外函數生成的多個閉包內存空間彼此獨立。
閉包應用場景:
在內存中維持變量:如果緩存數據、柯里化
保護函數內的變量安全:如迭代器、生成器。
缺點:閉包會導致原有的作用域鏈不釋放,造成內存的泄漏。
內存消耗有負?影響。因內部函數保存了對外部變量的引?,導致?法被垃圾回收,增?內存使?量,所以使? 不當會導致內存泄漏
對處理速度具有負?影響。閉包的層級決定了引?的外部變量在查找時經過的作?域鏈?度
可能獲取到意外的值(captured value)
優點:
可以從內部函數訪問外部函數的作?域中的變量,且訪問到的變量?期駐扎在內存中,可供之后使?
避免變量污染全局
把變量存到獨?的作?域,作為私有成員存在
一個函數和對其周圍狀態(lexical environment,詞法環境)的引用捆綁在一起(或者說函數被引用包圍),這樣的組合就是閉包(closure)。也就是說,閉包讓你可以在一個內層函數中訪問到其外層函數的作用域。在 JavaScript 中,每當創建一個函數,閉包就會在函數創建的同時被創建出來。
詞法作用域
請看下面的代碼:
function init() { var name = "Mozilla"; // name 是一個被 init 創建的局部變量 function displayName() { // displayName() 是內部函數,一個閉包 alert(name); // 使用了父函數中聲明的變量 } displayName(); } init();
init() 創建了一個局部變量 name 和一個名為 displayName() 的函數。displayName() 是定義在 init() 里的內部函數,并且僅在 init() 函數體內可用。請注意,displayName() 沒有自己的局部變量。然而,因為它可以訪問到外部函數的變量,所以 displayName() 可以使用父函數 init() 中聲明的變量 name 。
使用這個 JSFiddle 鏈接運行該代碼后發現, displayName() 函數內的 alert() 語句成功顯示出了變量 name 的值(該變量在其父函數中聲明)。這個詞法作用域的例子描述了分析器如何在函數嵌套的情況下解析變量名。詞法(lexical)一詞指的是,詞法作用域根據源代碼中聲明變量的位置來確定該變量在何處可用。嵌套函數可訪問聲明于它們外部作用域的變量。
基本數據類型的變量的值一般存在棧內存中,基本的數據類型: Number 、Boolean、Undefined、String、Null。;而對象類型的變量的值存儲在堆內存中,棧內存存儲對應空間地址。
var a = 1 //a是一個基本數據類型 var b = {m: 20 } //b是一個對象
對應內存存儲:
當我們執行 b={m:30}時,堆內存就有新的對象{m:30},棧內存的b指向新的空間地址( 指向{m:30} ),而堆內存中原來的{m:20}就會被程序引擎垃圾回收掉,節約內存空間。我們知道js函數也是對象,它也是在堆與棧內存中存儲的,我們來看一下轉化:
var a = 1; function fn(){ var b = 2 function fn1(){ console.log(b) } fn1() } fn()
棧是一種先進后出的數據結構:
在執行fn前,此時我們在全局執行環境(瀏覽器就是window作用域),全局作用域里有個變量a;
進入fn,此時棧內存就會push一個fn的執行環境,這個環境里有變量b和函數對象fn1,這里可以訪問自身執行環境和全局執行環境所定義的變量
進入fn1,此時棧內存就會push 一個fn1的執行環境,這里面沒有定義其他變量,但是我們可以訪問到fn和全局執行環境里面的變量,因為程序在訪問變量時,是向底層棧一個個找(這就是Javascript語言特有的"鏈式作用域"結構(chain scope)),如果找到全局執行環境里都沒有對應變量,則程序拋出underfined的錯誤。
隨著fn1()執行完畢,fn1的執行環境被杯銷毀,接著執行完fn(),fn的執行環境也會被銷毀,只剩全局的執行環境下,現在沒有b變量,和fn1函數對象了,只有a 和 fn(函數聲明作用域是window下)
在函數內訪問某個變量是根據函數作用域鏈來判斷變量是否存在的,而函數作用域鏈是程序根據函數所在的執行環境棧來初始化的,所以上面的例子,我們在fn1里面打印變量b,根據fn1的作用域鏈的找到對應fn執行環境下的變量b。所以當程序在調用某個函數時,做了一下的工作:準備執行環境,初始函數作用域鏈和arguments參數對象
我們現在看下閉包例子
function outer() { var a = '變量1' var inner = function () { console.info(a) } return inner // inner 就是一個閉包函數,因為他能夠訪問到outer函數的作用域 } var inner = outer() // 獲得inner閉包函數 inner() //"變量1"
當程序執行完var inner = outer(),其實outer的執行環境并沒有被銷毀,因為他里面的變量a仍然被被inner的函數作用域鏈所引用,當程序執行完inner(), 這時候,inner和outer的執行環境才會被銷毀調;《JavaScript高級編程》書中建議:由于閉包會攜帶包含它的函數的作用域,因為會比其他函數占用更多內容,過度使用閉包,會導致內存占用過多。
下面通過outer外函數和inner內函數來講解閉包的共享變量問題。
同一個外函數生成的多個閉包是獨立空間還是共享空間如何判斷?請先看實例
//第一種情況 調用時給外函數傳入變量值 function outer(name){ return function(){ console.log(name) } } f1 = outer('yang') f2 = outer('fang') console.log(f1.toString()) f1() //yang f2() //fang f1() //yang //第二種情況:外函數局部變量值為變化 function count() { var arr = []; for (var i=1; i<=3; i++) { arr.push(function () { return i * i; }); } return arr; } var results = count(); var f1 = results[0]; //16 var f2 = results[1]; //16 var f3 = results[2]; //16 console.log(f1 ) //第三種情況:外函數的局部變量值變化。 function test(){ var i = 0; return function(){ console.log(i++) } }; var a = test(); var b = test(); //依次執行a,a,b,控制臺會輸出什么呢?0 1 0 //b為什么不是2 a();a();b();
同一個外函數生成的多個閉包是獨立空間還是共享空間如何判斷?
第一種情況說明多次調用外函數生成的不同閉包函數沒有共享name變量
第二種情況說明外函數內部循環生成的多個內函數共享 i 局部變量
第三種情況說明a 、b為兩個 不同閉包 函數,同一閉包函數 a 多次調用 共享 i 變量,a b之間不共享。
可以總結出記住三個閉包共享變量的原則
調用外函數,就會生成內函數和外函數的局部變量組成的閉包。每調用一次生成一個閉包函數。不同閉包函數之間內存空間彼此獨立。
調用同一個閉包函數多次,共享內存空間,即外函數的局部變量值。
第二種情況for循環。沒有調用外函數,只是將內函數存到了數組中,故并沒有生成3個獨立的閉包函數。而是3個內函數共享一個外函數局部變量,即3個內函數和外函數局部變量組成了一個整體的閉包環境。
簡記:調用一次外函數,生成一個獨立的閉包環境;外函數內部生成多個內函數,那么多個內函數共用一個閉包環境。
應用場景主要就兩個
在內存中維持變量:如果緩存數據、柯里化
保護函數內的變量安全:如迭代器、生成器。
場景一:保存局部變量在內存中
閉包很有用,因為它允許將函數與其所操作的某些數據(環境)關聯起來。這顯然類似于面向對象編程。在面向對象編程中,對象允許我們將某些數據(對象的屬性)與一個或者多個方法相關聯。
因此,通常你使用只有一個方法的對象的地方,都可以使用閉包。
在 Web 中,你想要這樣做的情況特別常見。大部分我們所寫的 JavaScript 代碼都是基于事件的 — 定義某種行為,然后將其添加到用戶觸發的事件之上(比如點擊或者按鍵)。我們的代碼通常作為回調:為響應事件而執行的函數。
假如,我們想在頁面上添加一些可以調整字號的按鈕。一種方法是以像素為單位指定 body 元素的 font-size,然后通過相對的 em 單位設置頁面中其它元素(例如header)的字號:
body { font-family: Helvetica, Arial, sans-serif; font-size: 12px; } h2 { font-size: 1.5em; } h3 { font-size: 1.2em; }
我們的文本尺寸調整按鈕可以修改 body 元素的 font-size 屬性,由于我們使用相對單位,頁面中的其它元素也會相應地調整。
以下是 JavaScript:
function makeSizer(size) { return function() { document.body.style.fontSize = size + 'px'; }; } var size12 = makeSizer(12); var size14 = makeSizer(14); var size16 = makeSizer(16);
size12,size14 和 size16 三個函數將分別把 body 文本調整為 12,14,16 像素。我們可以將它們分別添加到按鈕的點擊事件上。如下所示:
document.getElementById('size-12').onclick = size12; document.getElementById('size-14').onclick = size14; document.getElementById('size-16').onclick = size16; <a href="#" id="size-12">12</a> <a href="#" id="size-14">14</a> <a href="#" id="size-16">16</a>
場景二:用閉包模擬私有方法,保護局部變量
編程語言中,比如 Java,是支持將方法聲明為私有的,即它們只能被同一個類中的其它方法所調用。
而 JavaScript 沒有這種原生支持,但我們可以使用閉包來模擬私有方法。私有方法不僅僅有利于限制對代碼的訪問:還提供了管理全局命名空間的強大能力,避免非核心的方法弄亂了代碼的公共接口部分。
下面的示例展現了如何使用閉包來定義公共函數,并令其可以訪問私有函數和變量。這個方式也稱為 模塊模式(module pattern):
var Counter = (function() { var privateCounter = 0; function changeBy(val) { privateCounter += val; } return { increment: function() { changeBy(1); }, decrement: function() { changeBy(-1); }, value: function() { return privateCounter; } } })(); console.log(Counter.value()); /* logs 0 */ Counter.increment(); Counter.increment(); console.log(Counter.value()); /* logs 2 */ Counter.decrement(); console.log(Counter.value()); /* logs 1 */
可以將上面的代碼拆分成兩部分:(function(){}) 和 () 。第1個() 是一個表達式,而這個表達式本身是一個匿名函數,所以在這個表達式后面加 () 就表示執行這個匿名函數。
在之前的示例中,每個閉包都有它自己的詞法環境;而這次我們只創建了一個詞法環境,為三個函數所共享:Counter.increment,Counter.decrement 和 Counter.value。
該共享環境創建于一個立即執行的匿名函數體內。這個環境中包含兩個私有項:名為 privateCounter 的變量和名為 changeBy 的函數。這兩項都無法在這個匿名函數外部直接訪問。必須通過匿名函數 返回的三個公共函數訪問。
這三個公共函數是共享同一個環境的閉包。多虧 JavaScript 的詞法作用域,它們都可以訪問 privateCounter 變量和 changeBy 函數。
你應該注意到我們定義了一個匿名函數,用于創建一個計數器。我們立即執行了這個匿名函數,并將他的值賦給了變量Counter。我們可以把這個函數儲存在另外一個變量makeCounter中,并用他來創建多個計數器。 var makeCounter = function() { var privateCounter = 0; function changeBy(val) { privateCounter += val; } return { increment: function() { changeBy(1); }, decrement: function() { changeBy(-1); }, value: function() { return privateCounter; } } }; var Counter1 = makeCounter(); var Counter2 = makeCounter(); console.log(Counter1.value()); /* logs 0 */ Counter1.increment(); Counter1.increment(); console.log(Counter1.value()); /* logs 2 */ Counter1.decrement(); console.log(Counter1.value()); /* logs 1 */ console.log(Counter2.value()); /* logs 0 */
請注意兩個計數器 Counter1 和 Counter2 是如何維護它們各自的獨立性的。每個閉包都是引用自己詞法作用域內的變量 privateCounter 。
每次調用其中一個計數器時,通過改變這個變量的值,會改變這個閉包的詞法環境。然而在一個閉包內對變量的修改,不會影響到另外一個閉包中的變量。
以這種方式使用閉包,提供了許多與面向對象編程相關的好處 —— 特別是數據隱藏和封裝。
在 ECMAScript 2015 引入 let 關鍵字 之前,在循環中有一個常見的閉包創建錯誤。參考下面的示例:
<p id="help">Helpful notes will appear here</p> <p>E-mail: <input type="text" id="email" name="email"></p> <p>Name: <input type="text" id="name" name="name"></p> <p>Age: <input type="text" id="age" name="age"></p> function showHelp(help) { document.getElementById('help').innerHTML = help; } function setupHelp() { var helpText = [ {'id': 'email', 'help': 'Your e-mail address'}, {'id': 'name', 'help': 'Your full name'}, {'id': 'age', 'help': 'Your age (you must be over 16)'} ]; for (var i = 0; i < helpText.length; i++) { var item = helpText[i]; document.getElementById(item.id).onfocus = function() { showHelp(item.help); } } } setupHelp();
//一、將function直接返回,會發生閉包 //二、將函數賦值給一個變量,此變量函數外部使用,此時也是閉包。比如,數組、多個變量等。 舉例下面也是閉包情況。 var arr = [] for (var i = 0; i < 10; i++) { arr[i] = function(){console.log(i)} } arr[6]()此時也是閉包,將十個匿名函數+i組成了一個閉包返回。
數組 helpText 中定義了三個有用的提示信息,每一個都關聯于對應的文檔中的input 的 ID。通過循環這三項定義,依次為相應input添加了一個 onfocus 事件處理函數,以便顯示幫助信息。
運行這段代碼后,您會發現它沒有達到想要的效果。無論焦點在哪個input上,顯示的都是關于年齡的信息。
原因是賦值給 onfocus 的是閉包。這些閉包是由他們的函數定義和在 setupHelp 作用域中捕獲的環境所組成的。這三個閉包在循環中被創建,但他們共享了同一個詞法作用域,在這個作用域中存在一個變量item。這是因為變量item使用var進行聲明,由于變量提升,所以具有函數作用域。當onfocus的回調執行時,item.help的值被決定。由于循環在事件觸發之前早已執行完畢,變量對象item(被三個閉包所共享)已經指向了helpText的最后一項。
解決這個問題的一種方案是使用更多的閉包:特別是使用前面所述的函數工廠:
function showHelp(help) { document.getElementById('help').innerHTML = help; } function makeHelpCallback(help) { return function() { showHelp(help); }; } function setupHelp() { var helpText = [ {'id': 'email', 'help': 'Your e-mail address'}, {'id': 'name', 'help': 'Your full name'}, {'id': 'age', 'help': 'Your age (you must be over 16)'} ]; for (var i = 0; i < helpText.length; i++) { var item = helpText[i]; document.getElementById(item.id).onfocus = makeHelpCallback(item.help); } } setupHelp();
這段代碼可以如我們所期望的那樣工作。所有的回調不再共享同一個環境, makeHelpCallback 函數為每一個回調創建一個新的詞法環境。在這些環境中,help 指向 helpText 數組中對應的字符串。
另一種方法使用了匿名閉包:
function showHelp(help) { document.getElementById('help').innerHTML = help; } function setupHelp() { var helpText = [ {'id': 'email', 'help': 'Your e-mail address'}, {'id': 'name', 'help': 'Your full name'}, {'id': 'age', 'help': 'Your age (you must be over 16)'} ]; for (var i = 0; i < helpText.length; i++) { (function() { var item = helpText[i]; document.getElementById(item.id).onfocus = function() { showHelp(item.help); } })(); // 馬上把當前循環項的item與事件回調相關聯起來 } } setupHelp();
如果不想使用過多的閉包,你可以用ES2015引入的let關鍵詞:
function showHelp(help) { document.getElementById('help').innerHTML = help; } function setupHelp() { var helpText = [ {'id': 'email', 'help': 'Your e-mail address'}, {'id': 'name', 'help': 'Your full name'}, {'id': 'age', 'help': 'Your age (you must be over 16)'} ]; for (var i = 0; i < helpText.length; i++) { let item = helpText[i]; document.getElementById(item.id).onfocus = function() { showHelp(item.help); } } } setupHelp();
這個例子使用let而不是var,因此每個閉包都綁定了塊作用域的變量,這意味著不再需要額外的閉包。
另一個可選方案是使用 forEach()來遍歷helpText數組,如下所示:
function showHelp(help) { document.getElementById('help').innerHTML = help; } function setupHelp() { var helpText = [ {'id': 'email', 'help': 'Your e-mail address'}, {'id': 'name', 'help': 'Your full name'}, {'id': 'age', 'help': 'Your age (you must be over 16)'} ]; helpText.forEach(function(text) { document.getElementById(text.id).onfocus = function() { showHelp(text.help); } }); } setupHelp();
如果不是某些特定任務需要使用閉包,在其它函數中創建函數是不明智的,因為閉包在處理速度和內存消耗方面對腳本性能具有負面影響。
但是如果某個函數需要不停新建,那么使用閉包保存到內存中對性能有好處。
釋放閉包只需要將引用閉包的函數置為null即可。
第一:多個內函數引用同一局部變量
function outer() { var result = []; for (var i = 0; i<10; i++){ result.[i] = function () { console.info(i) } } return result }
看樣子result每個閉包函數對打印對應數字,1,2,3,4,...,10, 實際不是,因為每個閉包函數訪問變量i是outer執行環境下的變量i,隨著循環的結束,i已經變成10了,所以執行每個閉包函數,結果打印10, 10, ..., 10
怎么解決這個問題呢?
function outer() { var result = []; for (var i = 0; i<10; i++){ result.[i] = function (num) { return function() { console.info(num); // 此時訪問的num,是上層函數執行環境的num,數組有10個函數對象,每個對象的執行環境下的number都不一樣 } }(i) } return result }
第二: this指向問題
var object = { name: ''object", getName: function() { return function() { console.info(this.name) } } } object.getName()() // underfined // 因為里面的閉包函數是在window作用域下執行的,也就是說,this指向windows
第三:內存泄露問題
function showId() { var el = document.getElementById("app") el.onclick = function(){ aler(el.id) // 這樣會導致閉包引用外層的el,當執行完showId后,el無法釋放 } } // 改成下面 function showId() { var el = document.getElementById("app") var id = el.id el.onclick = function(){ aler(id) // 這樣會導致閉包引用外層的el,當執行完showId后,el無法釋放 } el = null // 主動釋放el }
以上就是關于“JavaScript閉包如何理解”這篇文章的內容,相信大家都有了一定的了解,希望小編分享的內容對大家有幫助,若想了解更多相關的知識內容,請關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。