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