JavaScript中塊級(jí)作用域與函數(shù)作用域深入剖析
面試官必問(wèn)系列:深入理解JavaScript塊和函數(shù)作用域
在 JavaScript 中,究竟是什么會(huì)生成一個(gè)新的作用域,只有函數(shù)才會(huì)生成新的作用域嗎?那 JavaScript 其他結(jié)構(gòu)能生成新的作用域嗎?
函數(shù)中的作用域
在之前的詞法作用域中可見(jiàn) JavaScript 具有基于函數(shù)的作用域,這也就意味著一個(gè)函數(shù)都會(huì)創(chuàng)建一個(gè)新的作用域。但其實(shí)并不完全正確,看以下例子:
function foo(a) { var b = 2; function bar() { // ... } var c = 3; }
- 以上代碼片段中,foo() 的作用域中包含了標(biāo)識(shí)符 a, b, c 和 bar。無(wú)論表示聲明出現(xiàn)在作用域中的何處,這個(gè)標(biāo)識(shí)符所代表的變量和函數(shù)都附屬于所處作用域的作用域中。
- bar() 中也擁有屬于自己的作用域,全局作用域也有屬于自己的作用域,它只包含了一個(gè)標(biāo)識(shí)符: foo()
由于標(biāo)識(shí)符 a, b, c 和 bar 都附屬于 foo() 的作用域內(nèi),因此無(wú)法從 foo() 的外部對(duì)它們進(jìn)行訪問(wèn)。也就是說(shuō),這些標(biāo)識(shí)符在全局作用域中是無(wú)法被訪問(wèn)到的,因此如下代碼會(huì)拋出 ReferenceError:
bar(); // ReferenceError: bar is not defined console.log(a, b, c); // 全都拋出 ReferenceError
- 但標(biāo)識(shí)符 a, b, c 和 bar 可在 foo() 的內(nèi)部被訪問(wèn)的。
- 函數(shù)作用域的含義:屬于這個(gè)函數(shù)的全部變量都可以在整個(gè)函數(shù)的范圍內(nèi)使用及復(fù)用(在嵌套的作用域中也可以使用)。這種設(shè)計(jì)方案可根據(jù)需要改變值類(lèi)型的 "動(dòng)態(tài)" 特性。
隱藏內(nèi)部實(shí)現(xiàn)
- 我們對(duì)函數(shù)的傳統(tǒng)認(rèn)知就是先聲明一個(gè)函數(shù),然后再向里面添加代碼,但反過(guò)來(lái)可帶來(lái)一些啟示:從所寫(xiě)的代碼中挑選出一個(gè)任意片段,然后就用函數(shù)聲明的方式對(duì)它進(jìn)行包裝,實(shí)際上就是把這些代碼 "隱藏" 起來(lái)了。
- 實(shí)際的結(jié)果就是在這個(gè)代碼片段的周?chē)鷦?chuàng)建了一個(gè)新的作用域,也就是說(shuō)這段代碼中的任何聲明(變量或函數(shù))都將綁定在這個(gè)新創(chuàng)建的函數(shù)作用域中,而不是先前所在的作用域中。換句話說(shuō),可把變量和函數(shù)包裹在一個(gè)函數(shù)的作用域中,然后用這個(gè)作用域來(lái) "隱藏" 他們。
為什么 "隱藏" 變量和函數(shù)是一個(gè)有用的技術(shù)?
function doSomething(a) { b = a + doSomethingElse( a * 2 ); console.log( b * 3 ); } function doSomethingElse(a) { return a - 1; } var b; doSomething( 2 ); // 15
- 上述代碼片段中,變量 b 和函數(shù) doSomethingElse(..) 應(yīng)該是 doSomething(..) 內(nèi)部具體實(shí)現(xiàn)的 "私有" 內(nèi)容。而上述代碼將變量 b 和函數(shù) doSomethingElse(..) 的訪問(wèn)權(quán)限放在了外部作用域中,這可能是 "危險(xiǎn)" 的。更 "合理" 的設(shè)計(jì)應(yīng)該是將這些私有內(nèi)容放在 doSomething(...) 的內(nèi)部。
如下:
function doSomething(a) { function doSomethingElse(a) { return a - 1; } var b; b = a + doSomethingElse( a * 2 ); console.log( b * 3 ); } doSomething( 2 ); // 15
規(guī)避沖突
- "隱藏" 作用域中的變量和函數(shù)的另一個(gè)好處是可避免同名標(biāo)識(shí)符的沖突,兩個(gè)標(biāo)識(shí)符名字相同但用途不同,無(wú)意間可能會(huì)造成命名沖突,而沖突會(huì)導(dǎo)致變量的值被意外覆蓋。
例如:
function foo() { function bar(a) { i = 3; // 修改for 循環(huán)所屬作用域中的i console.log( a + i ); } for (var i=0; i<10; i++) { bar( i * 2 ); // 糟糕,無(wú)限循環(huán)了! } } foo();
bar(...) 內(nèi)部的賦值表達(dá)式 i = 3 意外地覆蓋了聲明在 foo(..) 內(nèi)部 for 循環(huán)中的 i。在這個(gè)例子中將會(huì)導(dǎo)致無(wú)限循環(huán),因?yàn)?i 被固定設(shè)置為 3,永遠(yuǎn)滿足小于 10 這個(gè)條件。
規(guī)則沖突的方式:
全局命名空間:在全局作用域中聲明一個(gè)足夠獨(dú)特的變量,通常為一個(gè)對(duì)象,如下:
var MyReallyCoolLibrary = { awesome: "stuff", doSomething: function() { // ... }, doAnotherThing: function() { // ... } }
模塊管理
函數(shù)作用域
- 現(xiàn)在知道,在任意代碼片段外部添加包裝函數(shù),可將內(nèi)部的變量和函數(shù)定義 "隱藏" 起來(lái),外部作用域無(wú)法訪問(wèn)包裝函數(shù)內(nèi)部的任何內(nèi)容。
如下:
var a = 2; function foo() { // <-- 添加這一行 var a = 3; console.log( a ); // 3 } // <-- 以及這一行 foo(); // <-- 以及這一行 console.log( a ); // 2
- 上述代碼會(huì)導(dǎo)致一些額外的問(wèn)題,首先,必需先聲明一個(gè)具名函數(shù) foo(), 這就意味著 foo 這個(gè)名稱(chēng)本身 "污染" 了所在作用域(上述代碼為全局作用域)。其次,必須顯式地通過(guò) foo() 來(lái)調(diào)用這個(gè)函數(shù)。
- 如果函數(shù)不需要函數(shù)名(或者至少函數(shù)名可以不污染所在作用域),且能夠自行運(yùn)行,這將會(huì)更理想。
JavaScript 提供了兩種方案來(lái)解決:
var a = 2; (function foo() { // <-- 添加這一行 var a = 3; console.log(a); // 3 })(); // <-- 以及這一行 console.log(a); // 2
- 在上述代碼中,包裝函數(shù)的聲明以 (function... 而不僅是以 function... 開(kāi)始。函數(shù)會(huì)被當(dāng)做函數(shù)表達(dá)式而不是一個(gè)標(biāo)準(zhǔn)的函數(shù)聲明來(lái)處理。
如何區(qū)分函數(shù)聲明和表達(dá)式?
- 最簡(jiǎn)單的方式就是看 function 關(guān)鍵字出現(xiàn)在聲明中的位置(不僅僅是一行代碼,而是整個(gè)聲明中的位置)。如果 function 為聲明中的第一個(gè)關(guān)鍵字,那它就是一個(gè)函數(shù)聲明,否則就是一個(gè)函數(shù)表達(dá)式。
- 函數(shù)聲明和函數(shù)表達(dá)式之間最重要的區(qū)別就是他們的名稱(chēng)標(biāo)識(shí)符將會(huì)綁定在何處。
- 比較一下前面兩個(gè)代碼片段。第一個(gè)片段中 foo 被綁定在所在作用域中,可以直接通過(guò) foo() 來(lái)調(diào)用它。第二個(gè)片段中foo 被綁定在函數(shù)表達(dá)式自身的函數(shù)中而不是所在作用域中。
- 換句話說(shuō),(function foo(){...}) 作為函數(shù)表達(dá)式意味著 foo 只能在 ... 所代表的位置中被訪問(wèn),外部作用域則不行。
匿名和具名
對(duì)于函數(shù)表達(dá)式最熟悉的就是回調(diào)參數(shù)了,如下:
setTimeout(function () { console.log("I waited 1 second!"); }, 1000);
- 這叫作匿名函數(shù)表達(dá)式,因?yàn)?nbsp;function().. 沒(méi)有名稱(chēng)標(biāo)識(shí)符。函數(shù)表達(dá)式可以是匿名的,而函數(shù)聲明則不可以省略函數(shù)名——在JavaScript 的語(yǔ)法中這是非法的。
匿名函數(shù)表達(dá)式的缺點(diǎn):
匿名函數(shù)在棧追蹤中不會(huì)顯示出有意義的函數(shù)名,這使調(diào)試很困難。
如果沒(méi)有函數(shù)名,當(dāng)函數(shù)需要引用自身時(shí)只能通過(guò)已經(jīng)過(guò)期的 arguments.callee 來(lái)引用。
匿名函數(shù)對(duì)代碼可讀性不是很友好。
上述代碼的改造結(jié)果:
setTimeout(function timeoutHandler() { console.log("I waited 1 second!"); }, 1000);
立即執(zhí)行函數(shù)表達(dá)式
var a = 2; (function IIFE() { var a = 3; console.log(a); // 3 })(); console.log(a); // 2
- 由于函數(shù)被包含在一對(duì)( ) 括號(hào)內(nèi)部,因此成為了一個(gè)表達(dá)式,通過(guò)在末尾加上另外一個(gè)( ) 可以立即執(zhí)行這個(gè)函數(shù),比如(function foo(){ .. })()。第一個(gè)( ) 將函數(shù)變成表達(dá)式,第二個(gè)( ) 執(zhí)行了這個(gè)函數(shù)。
- 立即執(zhí)行函數(shù)表達(dá)式的術(shù)語(yǔ)為:IIFE(Immediately Invoked Function Expression);
IIFE 的應(yīng)用場(chǎng)景:
除了上述傳統(tǒng)的 IIFE 方式,還有另一個(gè)方式,如下:
var a = 2; (function IIFE() { var a = 3; console.log(a); // 3 }()); console.log(a); // 2
第一種形式中函數(shù)表達(dá)式被包含在 ( ) 中,然后在后面用另一個(gè) () 括號(hào)來(lái)調(diào)用。第二種形式中用來(lái)調(diào)用的 () 括號(hào)被移進(jìn)了用來(lái)包裝的 ( ) 括號(hào)中。
這兩種方式的選擇全憑個(gè)人喜好。
IIFE 還有一種進(jìn)階用法,就是把他們當(dāng)做函數(shù)調(diào)用并傳遞參數(shù)進(jìn)去,如下:
var a = 2; (function IIFE(global) { var a = 3; console.log(a); // 3 console.log(global.a); // 2 })(window); console.log(a); // 2
IIFE 的另一個(gè)應(yīng)用場(chǎng)景是解決 undefined 標(biāo)識(shí)符的默認(rèn)值被錯(cuò)誤覆蓋導(dǎo)致的異常。
IIFE 的另一種變化的用途是倒置代碼的運(yùn)行順序,將需要運(yùn)行的函數(shù)放在第二位,在IIFE執(zhí)行之后當(dāng)做參數(shù)傳遞進(jìn)去。
var a = 2; (function IIFE(def) { def(window); })(function def(global) { var a = 3; console.log(a); // 3 console.log(global.a); // 2 });
函數(shù)表達(dá)式 def 定義在片段的第二部分,然后當(dāng)做參數(shù)(這個(gè)參數(shù)也叫做 def)被傳遞 IIFE 函數(shù)定義的第一部分中。最后,參數(shù) def(也就是傳遞進(jìn)去的函數(shù))被調(diào)用,并將 window 傳入當(dāng)做 global 參數(shù)的值。
塊作用域
將一個(gè)參數(shù)命名為 undefined, 但在對(duì)應(yīng)的位置不傳入任何值,這樣就可以就保證在代碼塊中 undefined 標(biāo)識(shí)符的值為 undefined
undefined = true; // 給其他代碼挖了一個(gè)大坑!絕對(duì)不要這樣做! (function IIFE(undefined) { var a; if (a === undefined) { console.log("Undefined is safe here!"); } })();
如下:
for (var i = 0; i < 5; i++){ console.log(i); }
- 在 for 循環(huán)中定義了變量 i,通常是想在 for 循環(huán)內(nèi)部的上下文中使用 i, 而忽略 i 會(huì)綁定在外部作用域(函數(shù)或全局)中。
修改后:
var foo = true; if(foo) { var bar = foo * 2; bar = something(bar); console.log(bar); }
- 上述代碼中,變量 bar 僅在 if 的上下文中使用,將它聲明在 if 內(nèi)部中式非常一個(gè)清晰的結(jié)構(gòu)。
- 當(dāng)使用 var 聲明變量時(shí),它寫(xiě)在哪里都是一樣的,因?yàn)樗罱K都會(huì)屬于外部作用域。(這也就是變量提升)
with
- 在詞法作用域中介紹了 with 關(guān)鍵字,它不僅是一個(gè)難于理解的結(jié)構(gòu),同是也是一塊作用域的一個(gè)例子(塊作用域的一種形式),用 with 從對(duì)象中創(chuàng)建出的作用域僅在 with 所處作用域中有效。
try/catch
很少有人注意,JavaScript 在 ES3 規(guī)范 try/catch 的 catch 分句會(huì)創(chuàng)建一個(gè)塊作用域,其中聲明的變量?jī)H會(huì)在 catch 內(nèi)部有效。
try { undefined(); // 目的是讓他拋出一個(gè)異常 } catch (error) { console.log("error ------>", error); // TypeError: undefined is not a function } console.log("error ------>", error); // ReferenceError: error is not defined
- error 僅存在于 catch 分句內(nèi)部,當(dāng)視圖從別處引用它時(shí)會(huì)拋出錯(cuò)誤。
- 關(guān)于 catch 分句看起來(lái)只是一些理論,但還是會(huì)有一些有用的信息的,后續(xù)文章會(huì)提到。
let
- JavaScript 在 ES6 中引入了 let 關(guān)鍵字。
let 關(guān)鍵字將變量綁定到所處的任意作用域中(通常是 { ... } 內(nèi)部)。換句話說(shuō),let 聲明的變量隱式地了所在的塊作用域。
var foo = true; if(foo) { var bar = foo * 2; bar = something(bar); console.log(bar); } console.log(bar); // ReferenceError: bar is not defined
使用 let 進(jìn)行的聲明不會(huì)再塊作用域中進(jìn)行提升。聲明的代碼被運(yùn)行前,聲明并不 "存在"。
{ console.log(bar); // ReferenceError let bar = 2; }
1. 垃圾收集
- 另一個(gè)塊作用域很有用的原因和閉包中的內(nèi)存垃圾回收機(jī)制相關(guān)。
如下代碼:
function process(data) { // do something } var someObj = {}; process(someObj); var btn = document.getElementById('my_button'); btn.addEventListener('click', function click(evt) { console.log('clicked'); }, /*capturingPhase=*/false);
- click 函數(shù)的點(diǎn)擊回調(diào)并不需要 someReallyBigData 變量。理論上這意味著當(dāng) process(..) 執(zhí)行后,在內(nèi)存中占用大量空間的數(shù)據(jù)結(jié)構(gòu)就可以被垃圾回收了。但是,由于 click函數(shù)形成了一個(gè)覆蓋整個(gè)作用域的閉包,JavaScript 引擎極有可能依然保存著這個(gè)結(jié)構(gòu)(取決于具體實(shí)現(xiàn))。
修改后:
function process(data) { // do something } // 在這個(gè)塊中定義內(nèi)容就可以銷(xiāo)毀了 { var someObj = {}; process(someObj); } var btn = document.getElementById('my_button'); btn.addEventListener('click', function click(evt) { console.log('clicked'); }, /*capturingPhase=*/false);
2. let循環(huán)
代碼如下:
for(let i = 0; i < 10; i++) { console.log(i); }; console.log(i); // ReferenceError
- for 循環(huán)中的 let 不僅將 i 綁定了for 循環(huán)內(nèi)部的塊中,事實(shí)上他將其重新綁定到了循環(huán)的每一次迭代中,確保使用上一個(gè)循環(huán)迭代結(jié)束時(shí)的值重新進(jìn)行賦值。
下面通過(guò)另一種方式來(lái)說(shuō)明每次迭代時(shí)進(jìn)行重新綁定的行為;
{ let i; for(i = 0; i < 10; i++) { let j = i; // 每次迭代中重新綁定 console.log(j); }; }
- let 聲明附屬與一個(gè)新的作用域而不是當(dāng)前的函數(shù)作用域(也不屬于全局作用域)。
考慮一下代碼:
var foo = true, baz = 10; if (foo) { var bar = 3; if (baz > bar) { console.log( baz ); } // ... }
這段代碼可以簡(jiǎn)單地被重構(gòu)成下面的同等形式:
var foo = true, baz = 10; if (foo) { var bar = 3; // ... } if (baz > bar) { console.log( baz ); }
但是在使用塊級(jí)作用域的變量時(shí)需要注意以下變化:
var foo = true, baz = 10; if (foo) { let bar = 3; if (baz > bar) { // <-- 移動(dòng)代碼時(shí)不要忘了 bar! console.log( baz ); } }
const
ES6 還引入了 const, 同樣可用來(lái)創(chuàng)建塊級(jí)作用域,但其值是固定的(常量), 不可修改。
var foo = true; if (foo) { var a = 2; const b = 3; // 包含在 if 中的塊作用域常量 a = 3; // 正常 ! b = 4; // 錯(cuò)誤 ! } console.log( a ); // 3 console.log( b ); // ReferenceError!
小結(jié)
- 函數(shù)時(shí) JavaScript 中最常見(jiàn)的作用域單元。
- 塊作用域值的是變量和函數(shù)布局可以屬于所處的作用域,也可以屬于某個(gè)代碼塊(通常指 {...} 內(nèi)部)
- 從 ES3 開(kāi)始, try/catch 結(jié)構(gòu)在 catch 分句中具有塊作用域。
- 從 ES6 引入了 let,const 關(guān)鍵字來(lái)創(chuàng)建塊級(jí)作用域。
以上就是JavaScript中塊級(jí)作用域與函數(shù)作用域的詳細(xì)內(nèi)容,更多關(guān)于JavaScript塊級(jí)函數(shù)作用域的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Vite項(xiàng)目自動(dòng)添加eslint prettier源碼解讀
這篇文章主要為大家介紹了Vite項(xiàng)目自動(dòng)添加eslint prettier源碼解讀,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12微信小程序手勢(shì)操作之單觸摸點(diǎn)與多觸摸點(diǎn)
這篇文章主要介紹了微信小程序手勢(shì)操作之單觸摸點(diǎn)與多觸摸點(diǎn)的相關(guān)資料,需要的朋友可以參考下2017-03-03微信小程序中input標(biāo)簽詳解及簡(jiǎn)單實(shí)例
這篇文章主要介紹了微信小程序中input標(biāo)簽詳解及簡(jiǎn)單實(shí)例的相關(guān)資料,需要的朋友可以參考下2017-05-05微信小程序 保留小數(shù)(toFixed)詳細(xì)介紹
這篇文章主要介紹了 微信小程序 保留小數(shù)(toFixed)詳細(xì)介紹的相關(guān)資料,這里附有實(shí)例,幫助大家學(xué)習(xí)參考此部分知識(shí),需要的朋友可以參考下2016-11-11http proxy 對(duì)網(wǎng)絡(luò)請(qǐng)求進(jìn)行代理使用詳解
這篇文章主要為大家介紹了http proxy 對(duì)網(wǎng)絡(luò)請(qǐng)求進(jìn)行代理使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09微信小程序 判斷手機(jī)號(hào)的實(shí)現(xiàn)代碼
這篇文章主要介紹了微信小程序 判斷手機(jī)號(hào)的實(shí)現(xiàn)代碼的相關(guān)資料,需要的朋友可以參考下2017-04-04