您好,登錄后才能下訂單哦!
這篇文章主要講解了“JavaScript的閉包與變量作用域介紹”,文中的講解內(nèi)容簡單清晰,易于學(xué)習(xí)與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學(xué)習(xí)“JavaScript的閉包與變量作用域介紹”吧!
JavaScript 是一種非常面向函數(shù)的語言。它給了我們很大的自由度。在 JavaScript 中,我們可以隨時(shí)創(chuàng)建函數(shù),可以將函數(shù)作為參數(shù)傳遞給另一個(gè)函數(shù),并在完全不同的代碼位置進(jìn)行調(diào)用。
我們已經(jīng)知道函數(shù)可以訪問其外部的變量。
但是,如果在函數(shù)被創(chuàng)建之后,外部變量發(fā)生了變化會(huì)怎樣?函數(shù)會(huì)獲得新值還是舊值?
如果將函數(shù)作為參數(shù)傳遞并在代碼中的另一個(gè)位置調(diào)用它,該函數(shù)將訪問的是新位置的外部變量嗎?
讓我們通過本文來學(xué)習(xí)這些相關(guān)知識(shí),以了解在這些場景以及更復(fù)雜的場景下到底會(huì)發(fā)生什么。
我們將在這探討一下 let/const
在 JavaScript 中,有三種聲明變量的方式:let,const(現(xiàn)代方式),var(過去留下來的方式)。
在本文的示例中,我們將使用 let 聲明變量。用 const 聲明的變量的行為也相同(譯注:與 let 在作用域等特性上是相同的),因此,本文也涉及用 const 進(jìn)行變量聲明。舊的 var 與上面兩個(gè)有著明顯的區(qū)別,我們將在 舊時(shí)的 "var" 中詳細(xì)介紹。
如果在代碼塊 {...} 內(nèi)聲明了一個(gè)變量,那么這個(gè)變量只在該代碼塊內(nèi)可見。
例如:
{ // 使用在代碼塊外不可見的局部變量做一些工作 let message = "Hello"; // 只在此代碼塊內(nèi)可見 alert(message); // Hello } alert(message); // Error: message is not defined
我們可以使用它來隔離一段代碼,該段代碼執(zhí)行自己的任務(wù),并使用僅屬于自己的變量:
{ // 顯示 message let message = "Hello"; alert(message); } { // 顯示另一個(gè) message let message = "Goodbye"; alert(message); }
這里如果沒有代碼塊則會(huì)報(bào)錯(cuò)
請注意,如果我們使用 let 對已存在的變量進(jìn)行重復(fù)聲明,如果對應(yīng)的變量沒有單獨(dú)的代碼塊,則會(huì)出現(xiàn)錯(cuò)誤:
// 顯示 message let message = "Hello"; alert(message); // 顯示另一個(gè) message let message = "Goodbye"; // Error: variable already declared alert(message);
對于 if,for 和 while 等,在 {...} 中聲明的變量也僅在內(nèi)部可見:
if (true) { let phrase = "Hello!"; alert(phrase); // Hello! } alert(phrase); // Error, no such variable!
在這兒,當(dāng) if 執(zhí)行完畢,則下面的 alert 將看不到 phrase,因此會(huì)出現(xiàn)錯(cuò)誤。
太好了,因?yàn)檫@就允許我們創(chuàng)建特定于 if 分支的塊級(jí)局部變量。
對于 for 和 while 循環(huán)也是如此:
for (let i = 0; i < 3; i++) { // 變量 i 僅在這個(gè) for 循環(huán)的內(nèi)部可見 alert(i); // 0,然后是 1,然后是 2 } alert(i); // Error, no such variable
從視覺上看,let i 位于 {...} 之外。但是 for 構(gòu)造在這里很特殊:在其中聲明的變量被視為塊的一部分。
當(dāng)一個(gè)函數(shù)是在另一個(gè)函數(shù)中創(chuàng)建的時(shí),那么該函數(shù)就被稱為“嵌套”的。
在 JavaScript 中很容易實(shí)現(xiàn)這一點(diǎn)。
我們可以使用嵌套來組織代碼,比如這樣:
function sayHiBye(firstName, lastName) { // 輔助嵌套函數(shù)使用如下 function getFullName() { return firstName + " " + lastName; } alert( "Hello, " + getFullName() ); alert( "Bye, " + getFullName() ); }
這里創(chuàng)建的 嵌套 函數(shù) getFullName() 是為了更加方便。它可以訪問外部變量,因此可以返回全名。嵌套函數(shù)在 JavaScript 中很常見。
更有意思的是,可以返回一個(gè)嵌套函數(shù):作為一個(gè)新對象的屬性或作為結(jié)果返回。之后可以在其他地方使用。不論在哪里調(diào)用,它仍然可以訪問相同的外部變量。
下面的 makeCounter 創(chuàng)建了一個(gè) “counter” 函數(shù),該函數(shù)在每次調(diào)用時(shí)返回下一個(gè)數(shù)字:
function makeCounter() { let count = 0; return function() { return count++; }; } let counter = makeCounter(); alert( counter() ); // 0 alert( counter() ); // 1 alert( counter() ); // 2
盡管很簡單,但稍加變型就具有很強(qiáng)的實(shí)際用途,比如,用作 隨機(jī)數(shù)生成器 以生成用于自動(dòng)化測試的隨機(jī)數(shù)值。
這是如何運(yùn)作的呢?如果我們創(chuàng)建多個(gè)計(jì)數(shù)器,它們會(huì)是獨(dú)立的嗎?這里的變量是怎么回事?
理解這些內(nèi)容對于掌握 JavaScript 的整體知識(shí)很有幫助,并且對于應(yīng)對更復(fù)雜的場景也很有益處。因此,讓我們繼續(xù)深入探究。
為了使內(nèi)容更清晰,這里將分步驟進(jìn)行講解。
Step 1. 變量
在 JavaScript 中,每個(gè)運(yùn)行的函數(shù),代碼塊 {...} 以及整個(gè)腳本,都有一個(gè)被稱為 詞法環(huán)境(Lexical Environment) 的內(nèi)部(隱藏)的關(guān)聯(lián)對象。
詞法環(huán)境對象由兩部分組成:
鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)
環(huán)境記錄(Environment Record) —— 一個(gè)存儲(chǔ)所有局部變量作為其屬性(包括一些其他信息,例如 this 的值)的對象。
對 外部詞法環(huán)境 的引用,與外部代碼相關(guān)聯(lián)。
一個(gè)“變量”只是 環(huán)境記錄 這個(gè)特殊的內(nèi)部對象的一個(gè)屬性?!矮@取或修改變量”意味著“獲取或修改詞法環(huán)境的一個(gè)屬性”。
舉個(gè)例子,這段沒有函數(shù)的簡單的代碼中只有一個(gè)詞法環(huán)境:
這就是所謂的與整個(gè)腳本相關(guān)聯(lián)的 全局 詞法環(huán)境。
在上面的圖片中,矩形表示環(huán)境記錄(變量存儲(chǔ)),箭頭表示外部引用。全局詞法環(huán)境沒有外部引用,所以箭頭指向了 null。
隨著代碼開始并繼續(xù)運(yùn)行,詞法環(huán)境發(fā)生了變化。
這是更長的代碼:
右側(cè)的矩形演示了執(zhí)行過程中全局詞法環(huán)境的變化:
鴻蒙官方戰(zhàn)略合作共建——HarmonyOS技術(shù)社區(qū)
當(dāng)腳本開始運(yùn)行,詞法環(huán)境預(yù)先填充了所有聲明的變量。最初,它們處于“未初始化(Uninitialized)”狀態(tài)。這是一種特殊的內(nèi)部狀態(tài),這意味著引擎知道變量,但是在用 let 聲明前,不能引用它。幾乎就像變量不存在一樣。
然后 let phrase 定義出現(xiàn)了。它尚未被賦值,因此它的值為 undefined。從這一刻起,我們就可以使用變量了。
phrase 被賦予了一個(gè)值。
phrase 的值被修改。
現(xiàn)在看起來都挺簡單的,是吧?
變量是特殊內(nèi)部對象的屬性,與當(dāng)前正在執(zhí)行的(代碼)塊/函數(shù)/腳本有關(guān)。
操作變量實(shí)際上是操作該對象的屬性。
詞法環(huán)境是一個(gè)規(guī)范對象:
“詞法環(huán)境”是一個(gè)規(guī)范對象(specification object):它僅僅是存在于 編程語言規(guī)范 中的“理論上”存在的,用于描述事物如何運(yùn)作的對象。我們無法在代碼中獲取該對象并直接對其進(jìn)行操作。
但 JavaScript 引擎同樣可以優(yōu)化它,比如清除未被使用的變量以節(jié)省內(nèi)存和執(zhí)行其他內(nèi)部技巧等,但顯性行為應(yīng)該是和上述的無差。
一個(gè)函數(shù)其實(shí)也是一個(gè)值,就像變量一樣。
不同之處在于函數(shù)聲明的初始化會(huì)被立即完成。
當(dāng)創(chuàng)建了一個(gè)詞法環(huán)境(Lexical Environment)時(shí),函數(shù)聲明會(huì)立即變?yōu)榧从眯秃瘮?shù)(不像 let 那樣直到聲明處才可用)。
這就是為什么我們可以在(函數(shù)聲明)的定義之前調(diào)用函數(shù)聲明。
例如,這是添加一個(gè)函數(shù)時(shí)全局詞法環(huán)境的初始狀態(tài):
正常來說,這種行為僅適用于函數(shù)聲明,而不適用于我們將函數(shù)分配給變量的函數(shù)表達(dá)式,例如 let say = function(name)...。
在一個(gè)函數(shù)運(yùn)行時(shí),在調(diào)用剛開始時(shí),會(huì)自動(dòng)創(chuàng)建一個(gè)新的詞法環(huán)境以存儲(chǔ)這個(gè)調(diào)用的局部變量和參數(shù)。
例如,對于 say("John"),它看起來像這樣(當(dāng)前執(zhí)行位置在箭頭標(biāo)記的那一行上):
在這個(gè)函數(shù)調(diào)用期間,我們有兩個(gè)詞法環(huán)境:內(nèi)部一個(gè)(用于函數(shù)調(diào)用)和外部一個(gè)(全局):
內(nèi)部詞法環(huán)境與 say 的當(dāng)前執(zhí)行相對應(yīng)。它具有一個(gè)單獨(dú)的屬性:name,函數(shù)的參數(shù)。我們調(diào)用的是 say("John"),所以 name 的值為 "John"。
外部詞法環(huán)境是全局詞法環(huán)境。它具有 phrase 變量和函數(shù)本身。
內(nèi)部詞法環(huán)境引用了 outer。
當(dāng)代碼要訪問一個(gè)變量時(shí) —— 首先會(huì)搜索內(nèi)部詞法環(huán)境,然后搜索外部環(huán)境,然后搜索更外部的環(huán)境,以此類推,直到全局詞法環(huán)境。
如果在任何地方都找不到這個(gè)變量,那么在嚴(yán)格模式下就會(huì)報(bào)錯(cuò)(在非嚴(yán)格模式下,為了向下兼容,給未定義的變量賦值會(huì)創(chuàng)建一個(gè)全局變量)。
在這個(gè)示例中,搜索過程如下:
對于 name 變量,當(dāng) say 中的 alert 試圖訪問 name 時(shí),會(huì)立即在內(nèi)部詞法環(huán)境中找到它。
當(dāng)它試圖訪問 phrase 時(shí),然而內(nèi)部沒有 phrase,所以它順著對外部詞法環(huán)境的引用找到了它。
讓我們回到 makeCounter 這個(gè)例子。
function makeCounter() { let count = 0; return function() { return count++; }; } let counter = makeCounter();
在每次 makeCounter() 調(diào)用的開始,都會(huì)創(chuàng)建一個(gè)新的詞法環(huán)境對象,以存儲(chǔ)該 makeCounter 運(yùn)行時(shí)的變量。
因此,我們有兩層嵌套的詞法環(huán)境,就像上面的示例一樣:
不同的是,在執(zhí)行 makeCounter() 的過程中創(chuàng)建了一個(gè)僅占一行的嵌套函數(shù):return count++。我們尚未運(yùn)行它,僅創(chuàng)建了它。
所有的函數(shù)在“誕生”時(shí)都會(huì)記住創(chuàng)建它們的詞法環(huán)境。從技術(shù)上講,這里沒有什么魔法:所有函數(shù)都有名為 [[Environment]] 的隱藏屬性,該屬性保存了對創(chuàng)建該函數(shù)的詞法環(huán)境的引用。
因此,counter.[[Environment]] 有對 {count: 0} 詞法環(huán)境的引用。這就是函數(shù)記住它創(chuàng)建于何處的方式,與函數(shù)被在哪兒調(diào)用無關(guān)。[[Environment]] 引用在函數(shù)創(chuàng)建時(shí)被設(shè)置并永久保存。
稍后,當(dāng)調(diào)用 counter() 時(shí),會(huì)為該調(diào)用創(chuàng)建一個(gè)新的詞法環(huán)境,并且其外部詞法環(huán)境引用獲取于 counter.[[Environment]]:
現(xiàn)在,當(dāng) counter() 中的代碼查找 count 變量時(shí),它首先搜索自己的詞法環(huán)境(為空,因?yàn)槟抢餂]有局部變量),然后是外部 makeCounter() 的詞法環(huán)境,并且在哪里找到就在哪里修改。
在變量所在的詞法環(huán)境中更新變量。
這是執(zhí)行后的狀態(tài):
如果我們調(diào)用 counter() 多次,count 變量將在同一位置增加到 2,3 等。
閉包
開發(fā)者通常應(yīng)該都知道“閉包”這個(gè)通用的編程術(shù)語。
閉包 是指內(nèi)部函數(shù)總是可以訪問其所在的外部函數(shù)中聲明的變量和參數(shù),即使在其外部函數(shù)被返回(壽命終結(jié))了之后。在某些編程語言中,這是不可能的,或者應(yīng)該以特殊的方式編寫函數(shù)來實(shí)現(xiàn)。但是如上所述,在 JavaScript 中,所有函數(shù)都是天生閉包的(只有一個(gè)例外,將在 "new Function" 語法 中講到)。
也就是說:JavaScript 中的函數(shù)會(huì)自動(dòng)通過隱藏的 [[Environment]] 屬性記住創(chuàng)建它們的位置,所以它們都可以訪問外部變量。
在面試時(shí),前端開發(fā)者通常會(huì)被問到“什么是閉包?”,正確的回答應(yīng)該是閉包的定義,并解釋清楚為什么 JavaScript 中的所有函數(shù)都是閉包的,以及可能的關(guān)于 [[Environment]] 屬性和詞法環(huán)境原理的技術(shù)細(xì)節(jié)。
通常,函數(shù)調(diào)用完成后,會(huì)將詞法環(huán)境和其中的所有變量從內(nèi)存中刪除。因?yàn)楝F(xiàn)在沒有任何對它們的引用了。與 JavaScript 中的任何其他對象一樣,詞法環(huán)境僅在可達(dá)時(shí)才會(huì)被保留在內(nèi)存中。
但是,如果有一個(gè)嵌套的函數(shù)在函數(shù)結(jié)束后仍可達(dá),則它將具有引用詞法環(huán)境的 [[Environment]] 屬性。
在下面這個(gè)例子中,即使在函數(shù)執(zhí)行完成后,詞法環(huán)境仍然可達(dá)。因此,此嵌套函數(shù)仍然有效。
例如:
function f() { let value = 123; return function() { alert(value); } } let g = f(); // g.[[Environment]] 存儲(chǔ)了對相應(yīng) f() 調(diào)用的詞法環(huán)境的引用
請注意,如果多次調(diào)用 f(),并且返回的函數(shù)被保存,那么所有相應(yīng)的詞法環(huán)境對象也會(huì)保留在內(nèi)存中。下面代碼中有三個(gè)這樣的函數(shù):
function f() { let value = Math.random(); return function() { alert(value); }; } // 數(shù)組中的 3 個(gè)函數(shù),每個(gè)都與來自對應(yīng)的 f() 的詞法環(huán)境相關(guān)聯(lián) let arr = [f(), f(), f()];
當(dāng)詞法環(huán)境對象變得不可達(dá)時(shí),它就會(huì)死去(就像其他任何對象一樣)。換句話說,它僅在至少有一個(gè)嵌套函數(shù)引用它時(shí)才存在。
在下面的代碼中,嵌套函數(shù)被刪除后,其封閉的詞法環(huán)境(以及其中的 value)也會(huì)被從內(nèi)存中刪除:
function f() { let value = 123; return function() { alert(value); } } let g = f(); // 當(dāng) g 函數(shù)存在時(shí),該值會(huì)被保留在內(nèi)存中 g = null; // ……現(xiàn)在內(nèi)存被清理了
但在實(shí)際中,JavaScript 引擎會(huì)試圖優(yōu)化它。它們會(huì)分析變量的使用情況,如果從代碼中可以明顯看出有未使用的外部變量,那么就會(huì)將其刪除。
在 V8(Chrome,Edge,Opera)中的一個(gè)重要的副作用是,此類變量在調(diào)試中將不可用。
打開 Chrome 瀏覽器的開發(fā)者工具,并嘗試運(yùn)行下面的代碼。
當(dāng)代碼執(zhí)行暫停時(shí),在控制臺(tái)中輸入 alert(value)。
function f() { let value = Math.random(); function g() { debugger; // 在 Console 中:輸入 alert(value); No such variable! } return g; } let g = f(); g();
正如你所見的 —— No such variable! 理論上,它應(yīng)該是可以訪問的,但引擎把它優(yōu)化掉了。
這可能會(huì)導(dǎo)致有趣的(如果不是那么耗時(shí)的)調(diào)試問題。其中之一 —— 我們可以看到的是一個(gè)同名的外部變量,而不是預(yù)期的變量:
let value = "Surprise!"; function f() { let value = "the closest value"; function g() { debugger; // 在 console 中:輸入 alert(value); Surprise! } return g; } let g = f(); g();
V8 引擎的這個(gè)特性你真的應(yīng)該知道。如果你要使用 Chrome/Edge/Opera 進(jìn)行代碼調(diào)試,遲早會(huì)遇到這樣的問題。
這不是調(diào)試器的 bug,而是 V8 的一個(gè)特別的特性。也許以后會(huì)被修改。你始終可以通過運(yùn)行本文中的示例來進(jìn)行檢查。
感謝各位的閱讀,以上就是“JavaScript的閉包與變量作用域介紹”的內(nèi)容了,經(jīng)過本文的學(xué)習(xí)后,相信大家對JavaScript的閉包與變量作用域介紹這一問題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是億速云,小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。