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