JavaScript中什么是作用域幽邓?有多少種作用域距糖?為什么我要學(xué)習(xí)作用域?
相信很多小伙伴們都能明白“作用域”這三個(gè)字垒迂,按照字面上理解械姻,它不就是“發(fā)揮作用的區(qū)域”嗎?然而在我的前端學(xué)習(xí)中娇斑,我發(fā)現(xiàn)它有一個(gè)非常專(zhuān)業(yè)的解釋稱(chēng)為 執(zhí)行環(huán)境策添。
執(zhí)行環(huán)境是什么
執(zhí)行環(huán)境是JavaScript中最為重要的一個(gè)概念,因?yàn)樗x了變量或函數(shù)有權(quán)訪問(wèn)其他數(shù)據(jù)毫缆,并且決定了它們的行為,因此執(zhí)行環(huán)境擁有一套獨(dú)有的 規(guī)則乐导。接下來(lái)苦丁,我們?nèi)雮€(gè)門(mén)來(lái)了解下JavaScript是如何工作的?
編譯原理
JavaScript 引擎的編譯步驟物臂,可以說(shuō)是為我們后面理解執(zhí)行環(huán)境埋下了深厚的伏筆旺拉。在這里我將給大家介紹它的三個(gè)具體步驟:
-
詞法分析
這個(gè)過(guò)程會(huì)將由字符組成的字符串分解成(對(duì)編程語(yǔ)言來(lái)說(shuō))有意義的代碼塊产上。這些代碼塊被稱(chēng)為詞法單元。
例如:var a = 2;
蛾狗,這個(gè)字符串你會(huì)怎么理解晋涣?通常這段字符串會(huì)被分解成為下面這些詞法單元:var
、a
沉桌、=
谢鹊、2
、;留凭〉瓒螅空格是否會(huì)被當(dāng)作詞法單元,取決于空格在這門(mén)語(yǔ)言中是否具有意義蔼夜。
-
語(yǔ)法分析
這個(gè)過(guò)程是將詞法單元流(數(shù)組)轉(zhuǎn)換成一個(gè)由元素逐級(jí)嵌套所組成的代表了程序語(yǔ)法結(jié)構(gòu)的樹(shù)兼耀。這個(gè)樹(shù)被稱(chēng)為“抽象語(yǔ)法樹(shù)”(Abstract Syntax Tree,AST)求冷。
-
生成代碼
最后這個(gè)過(guò)程是將“抽象語(yǔ)法樹(shù)”(AST) 轉(zhuǎn)換為可執(zhí)行代碼的過(guò)程稱(chēng)被稱(chēng)為代碼生成瘤运。這個(gè)過(guò)程與語(yǔ)言、目標(biāo)平臺(tái)等息息相關(guān)匠题。 拋開(kāi)具體細(xì)節(jié)拯坟,簡(jiǎn)單來(lái)說(shuō)就是有某種方法可以將
var a = 2;
的 AST 轉(zhuǎn)化為一組機(jī)器指令,用來(lái)創(chuàng)建一個(gè)叫作 a 的變量(包括分配內(nèi)存等)梧躺,并將一個(gè)值儲(chǔ)存在 a 中似谁。
小結(jié):
var a = 2;
,意思就是先在我的編譯器中聲明這個(gè)變量 a掠哥,并為它分配內(nèi)存巩踏,緊接著我再賦個(gè)值給 a,這個(gè)值存在a里面续搀。
三位好伙伴
要參與到對(duì)程序 var a = 2;
進(jìn)行處理的過(guò)程塞琼,我們需要了解三位好家伙,分別是引擎禁舷、編譯器彪杉、作用域∏A可以說(shuō) JavaScript 工作都是靠它們?nèi)淮蠊Τ紒?lái)完成了派近,首先 引擎 從頭到尾負(fù)責(zé)整個(gè) JavaScript 程序的編譯及執(zhí)行過(guò)程,緊接著 編譯器 負(fù)責(zé)語(yǔ)法分析及代碼生成等臟活累活洁桌,最后 作用域 負(fù)責(zé)收集并維護(hù)由所有聲明的標(biāo)識(shí)符(變量)組成的一系列查 詢渴丸,并實(shí)施一套非常嚴(yán)格的規(guī)則,確定當(dāng)前執(zhí)行的代碼對(duì)這些標(biāo)識(shí)符的訪問(wèn)權(quán)限。
下面我們將 var a = 2;
分解谱轨,看看引擎和它的朋友們是如何協(xié)同工作的戒幔。
- 遇到
var a
,編譯器會(huì)詢問(wèn)作用域是否已經(jīng)有一個(gè)該名稱(chēng)的變量存在于同一個(gè)作用域的集合中土童。如果是诗茎,編譯器會(huì)忽略該聲明,繼續(xù)進(jìn)行編譯献汗;否則它會(huì)要求作用域在當(dāng)前作用域的集合中聲明一個(gè)新的變量敢订,并命名為 a。 - 接下來(lái)編譯器會(huì)為引擎生成運(yùn)行時(shí)所需的代碼雀瓢,這些代碼被用來(lái)處理
a = 2
這個(gè)賦值操作枢析。引擎運(yùn)行時(shí)會(huì)首先詢問(wèn)作用域,在當(dāng)前的作用域集合中是否存在一個(gè)叫作 a 的變量刃麸。如果是醒叁,引擎就會(huì)使用這個(gè)變量;如果否泊业,引擎會(huì)繼續(xù)查找該變量把沼。 - 如果引擎最終找到了 a 變量,就會(huì)將 2 賦值給它吁伺。否則引擎就會(huì)舉手示意并拋出一個(gè)Error饮睬!
LHS & RHS
LHS(Left-Hand-Search)稱(chēng)為左側(cè)查找,那么顧名思義篮奄,RHS(Right-Hand-Search)自然叫做右側(cè)查找了捆愁。在概念上最好將其理解為“賦值操作的目標(biāo)是誰(shuí)(LHS)”以及“誰(shuí)是賦值操作的源頭 (RHS)”。
考慮以下代碼你會(huì)更明白:
function foo(a) {
console.log(a); // 2
}
foo(2);
最后一行foo( 2 );
我們調(diào)用了 foo 函數(shù)窟却,并且傳給它了一個(gè)實(shí)參 2昼丑,對(duì)于 foo 函數(shù),它接受到了一個(gè)參數(shù)并把這個(gè)參數(shù)賦值給 a夸赫,因此a = 2
菩帝,這一步是左側(cè)查找了,可以理解為 a 是 2 要賦值的目標(biāo)茬腿。最后console.log( a );
這段話中呼奢,執(zhí)行的就是右側(cè)查找,因?yàn)槲覀円敵?a 就必須去查找這個(gè)值切平。
小結(jié): LHS 指的是等式左邊部分握础,RHS 指的是等式右邊部分
有了以上的基礎(chǔ)概念,我們不難理解原來(lái)作用域就是我代碼書(shū)寫(xiě)悴品、生成弓候、和執(zhí)行的地方啊郎哭,也就是代碼的執(zhí)行環(huán)境啊他匪。
作用域嵌套
當(dāng)一個(gè)塊或函數(shù)嵌套在另一個(gè)塊或函數(shù)中時(shí)菇存,就發(fā)生了作用域的嵌套。因此邦蜜,在當(dāng)前作用域中無(wú)法找到某個(gè)變量時(shí)依鸥,引擎就會(huì)在外層嵌套的作用域中繼續(xù)查找,直到找到該變量悼沈, 或抵達(dá)最外層的作用域(也就是全局作用域)為止贱迟。
考慮以下代碼:
function foo(a) {
console.log(a + b);
}
var b = 2;
foo(2); // 4
你會(huì)發(fā)現(xiàn)在我的 foo 函數(shù)中,我并沒(méi)有聲明 a絮供、b 變量衣吠,怎么辦?輸出會(huì)報(bào)錯(cuò)嗎壤靶?把代碼運(yùn)行一下缚俏,結(jié)果出乎意料居然是 4。這個(gè)結(jié)果到底是怎么來(lái)的贮乳?
- 前面我們剛講過(guò) LHS 和 RHS忧换,所以我們能理解當(dāng)我們調(diào)用 foo 函數(shù)時(shí)其實(shí)是進(jìn)行了一次 LHS,把 2 賦值給了 a向拆。
- 對(duì)于 b 的值是如何取到的亚茬,我們就要明白如果引擎在當(dāng)前作用域中無(wú)法找到某個(gè)變量時(shí),它就會(huì)在外層嵌套的作用域中繼續(xù)查找浓恳,在這里 foo 函數(shù)中并沒(méi)有 b 這個(gè)變量刹缝,因此引擎會(huì)向外查找,下一個(gè)作用域就是全局作用域了颈将,很慶幸梢夯,在全局作用域中有 b 這個(gè)變量,這是 foo 函數(shù)就可以獲得 b 變量的值 2吆鹤,最后輸出 4 了厨疙。
作用域類(lèi)別
作用域有三種,分為詞法作用域疑务,函數(shù)作用域和塊作用域沾凄。當(dāng)然,你也可以說(shuō)作用域有兩種知允,靜態(tài)作用域和動(dòng)態(tài)作用域撒蟀,那么這又是另一個(gè)話題了。
詞法作用域
簡(jiǎn)單地說(shuō)温鸽,詞法作用域就是定義在詞法階段的作用域保屯。換句話說(shuō)手负,詞法作用域是由你在寫(xiě)代碼時(shí)將變量和塊作用域?qū)懺谀睦飦?lái)決定的。
考慮以下代碼:
function foo(a) {
var b = a * 2;
function bar(c) {
console.log(a, b, c);
}
bar(b * 3);
}
foo(2); // 2, 4, 12
在這個(gè)例子中有三個(gè)逐級(jí)嵌套的作用域姑尺。為了幫助理解竟终,可以將它們想象成幾個(gè)逐級(jí)包含的氣泡。
查找 a切蟋、b统捶、c 依次從內(nèi)向外,a柄粹、b 都在 foo 函數(shù)作用域中找到喘鸟,c 在 bar 作用域中找到。作用域查是從最內(nèi)層的作用域開(kāi)始查找驻右,例如:bar -> foo -> global什黑,它會(huì)在找到第一個(gè)匹配的標(biāo)識(shí)符時(shí)停止。我們把 bar -> foo -> global 稱(chēng)為作用域鏈堪夭,引擎在查找時(shí)就是依照作用域鏈不斷向上查找的愕把。
函數(shù)作用域
函數(shù)作用域的含義是指,屬于這個(gè)函數(shù)的全部變量都可以在整個(gè)函數(shù)的范圍內(nèi)使用及復(fù)用(事實(shí)上在嵌套的作用域中也可以使用)茵瘾。
在學(xué)習(xí)詞法作用域的時(shí)候礼华,我們就已經(jīng)接觸到了函數(shù)作用域,但是我仍需要強(qiáng)調(diào)一點(diǎn)拗秘,函數(shù)作用域中的變量只能給自己和子函數(shù)使用圣絮,父級(jí)函數(shù)是獲取不到子函數(shù)中的變量的。
舉個(gè)例子:
function foo(a) {
function bar(c) {
c = 3;
}
console.log(a, c)
}
foo(2); // Uncaught ReferenceError: c is not defined
塊作用域
1. 零污染
盡管函數(shù)作用域是最常見(jiàn)的作用域單元雕旨,當(dāng)然也是現(xiàn)行大多數(shù) JavaScript 中最普遍的設(shè)計(jì)方法扮匠,但其他類(lèi)型的作用域單元也是存在的,并且通過(guò)使用其他類(lèi)型的作用域單元甚至可以實(shí)現(xiàn)維護(hù)起來(lái)更加優(yōu)秀凡涩、簡(jiǎn)潔的代碼棒搜。
for (var i = 0; i < 10; i++) {
console.log(i);
}
我們?cè)?for 循環(huán)的頭部直接定義了變量 i,通常是因?yàn)橹幌朐?for 循環(huán)內(nèi)部的上下文中使 用 i活箕,而忽略了 i 會(huì)被綁定在外部作用域(函數(shù)或全局)中的事實(shí)力麸。
使用塊作用域可以幫助我們避免污染全局作用域。
var foo = true;
if (foo) {
var bar = foo * 2;
bar = something(bar);
console.log(bar);
}
bar 變量?jī)H在 if 聲明的上下文中使用育韩,以為我們用 {} 把它包裹在了一個(gè)塊當(dāng)中克蚂,這樣就避免了不污染全局作用域。
如果你還是不能理解塊級(jí)作用域的好處筋讨,希望下面這個(gè)例子可以幫助你理解:
for (let i = 0; i < 10; i++) {
setTimeout(() => {
console.log(i);
}, 1000);
}
正常情況下埃叭,我們對(duì)這段代碼行為的預(yù)期是分別輸出數(shù)字 0~9,每秒一次悉罕,每次一個(gè)赤屋。但實(shí)際上立镶,這段代碼在運(yùn)行時(shí)會(huì)以每秒一次的頻率輸出10次 10。
這是為什么类早?
要理解 10 是怎么來(lái)的媚媒,我們需要理清以下兩點(diǎn):
- 由于一次又一次的循環(huán),循環(huán)結(jié)束時(shí)莺奔,i 此時(shí)已經(jīng)是 10 了欣范。換句話說(shuō),當(dāng) i 是 10 的時(shí)候令哟,循環(huán)結(jié)束。
- var 關(guān)鍵字聲明其實(shí)是一個(gè)全局聲明
這樣我們就很好理解這個(gè)結(jié)果了妨蛹。首先定時(shí)器會(huì)在 1s 中之后輸出屏富,此時(shí) i 已經(jīng)變成了 10,而 i 又是個(gè)全局變量蛙卤,所以此時(shí)輸出的會(huì)是 10 次 一樣的值狠半。
那我們?nèi)绾尾拍懿晃廴救肿饔糜蚰兀窟@里我推薦使用 let 關(guān)鍵字颤难,let 關(guān)鍵字可以將變量綁定到所在的任意作用域中(通常是 { .. } 內(nèi)部)神年。換句話說(shuō),let 為其聲明的變量隱式地了所在的塊作用域行嗤。(不了解 es6 中新增的 let 和 const 關(guān)鍵字可以先跳過(guò)這里往后看再回到這里)
如果我們用 let 替換 var 關(guān)鍵字已日,你會(huì)發(fā)現(xiàn)我們非常成功的獲取到了我們認(rèn)為的結(jié)果,原因是因?yàn)?let 關(guān)鍵字把變量 i 綁定在了我當(dāng)前這個(gè)作用域(循環(huán))中栅屏,因此全局作用域中并不會(huì)出現(xiàn) i 變量飘千,每次循化都會(huì)綁定一個(gè)獨(dú)立的值。
2. 垃圾收集
另一個(gè)塊作用域非常有用的原因和閉包及回收內(nèi)存垃圾的回收機(jī)制相關(guān)栈雳。在這里我們只看一個(gè)小小的例子护奈。
function process(data) {
// 在這里做點(diǎn)有趣的事情
}
// 在這個(gè)塊中定義的內(nèi)容可以銷(xiāo)毀了!
{
let someReallyBigData = {..
}
process(someReallyBigData);
}
var btn = document.getElementById("my_button");
btn.addEventListener("click", function click(evt) {
console.log("button clicked");
});
es6 新增的 let & const 關(guān)鍵字
let 關(guān)鍵字可以把變量綁定在當(dāng)前作用域中哥纫,外面作用域無(wú)法獲得這個(gè)變量霉旗。
for (let i = 0; i < 10; i++) {
console.log(i); // 0 ~ 9
}
console.log(i); // Uncaught ReferenceError: i is not defined
const,同樣可以用來(lái)創(chuàng)建塊作用域變量蛀骇,但其值是固定的(常量)厌秒。之后任何試圖修改值的操作都會(huì)引起錯(cuò)誤。
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!
雖然說(shuō)我們?cè)诼暶髯兞康臅r(shí)候用 var 沒(méi)有什么問(wèn)題松靡,但是一旦我們作用域中變量多起來(lái)简僧,我們的大腦可能就會(huì)處于一種混亂的狀態(tài),出現(xiàn) bug 時(shí)會(huì)不知所措(找 bug 是真的頭痛哦)雕欺!在這里我還是推薦小伙伴們可以盡量使用 let 或 const 這種新增的關(guān)鍵字岛马。
結(jié)語(yǔ)
看到這里我們就已經(jīng)學(xué)習(xí)完了 JavaScript 中作用域棉姐,之所以我們要了解并掌握作用域,最大的原因是它能幫助我們快速的分析代碼出錯(cuò)之處啦逆,它對(duì)我們的影響也會(huì)潛移默化的體現(xiàn)在我們書(shū)寫(xiě)的代碼當(dāng)中伞矩,相信小伙伴們未來(lái)都能擺脫 bug 煩惱!:)