JS入門難點(diǎn)解析3-作用域

(注1:如果有問(wèn)題歡迎留言探討,一起學(xué)習(xí)!轉(zhuǎn)載請(qǐng)注明出處,喜歡可以點(diǎn)個(gè)贊哦!)
(注2:更多內(nèi)容請(qǐng)查看我的目錄。)

1. 簡(jiǎn)介

在本系列的前一篇文章JS入門難點(diǎn)解析2-JS的變量提升和函數(shù)提升中,我們已經(jīng)討論過(guò)。之所以不說(shuō)JS需要編譯,只是它不像其他編譯語(yǔ)言一樣需要翻譯成等價(jià)的另一種語(yǔ)言。但是仍然需要進(jìn)行語(yǔ)法分析和代碼生成,并且通常是立即執(zhí)行。而且,JS的變量提升和函數(shù)提升就發(fā)生在編譯階段。

那么,在這里我們來(lái)思考一下,這些變量在哪里?換句話說(shuō),它們儲(chǔ)存在哪里?最重要的是,程序需要時(shí)如何找到它們?要解決這個(gè)問(wèn)題,我們需要一套規(guī)則來(lái)存儲(chǔ)變量,并且之后可以方便地找到這些變量。這套規(guī)則被稱為作用域。

2. 引擎,編譯,作用域

2.1 概念介紹

我們會(huì)在今后對(duì)引擎與編譯知識(shí)做深度探討,但在此處,只需要理解其概念與作用即可。如下:

  • 引擎
    從頭到尾負(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)限。

2.2 三者的關(guān)系

我們用一段代碼來(lái)揭示三者的關(guān)系。

var a = 2;

這段代碼是不是比你想的還要簡(jiǎn)單,僅僅是在聲明一個(gè)變量a的同時(shí)為其賦值2。那么,引擎和編譯器是如何處理這段代碼的呢?

事實(shí)上編譯器會(huì)進(jìn)行如下處理:

  1. 遇到 var a,編譯器會(huì)詢問(wèn)作用域是否已經(jīng)有一個(gè)該名稱的變量存在于同一個(gè)作用域的集合中。如果是,編譯器會(huì)忽略該聲明,繼續(xù)進(jìn)行編譯;否則它會(huì)要求作用域在當(dāng)前作用域的集合中聲明一個(gè)新的變量,并命名為 a。
  2. 接下來(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è)異常!

總結(jié):變量的賦值操作會(huì)執(zhí)行兩個(gè)動(dòng)作,首先編譯器會(huì)在當(dāng)前作用域中聲明一個(gè)變量(如果之前沒(méi)有聲明過(guò)),然后在運(yùn)行時(shí)引擎會(huì)在作用域鏈中查找該變量,如果能夠找到就會(huì)對(duì)它賦值。

編譯器在編譯過(guò)程的第二步中生成了代碼,引擎執(zhí)行它時(shí),會(huì)通過(guò)查找變量 a 來(lái)判斷它是 否已聲明過(guò)。查找的過(guò)程由作用域進(jìn)行協(xié)助,但是引擎執(zhí)行怎樣的查找,會(huì)影響最終的查找結(jié)果。那么引擎如何進(jìn)行查詢呢?

3. LHS和RHS

引擎的查詢方式有兩種,即LHS和RHS。變量出現(xiàn)在賦值操作的左側(cè)時(shí)進(jìn)行 LHS 查詢,出現(xiàn)在右側(cè)時(shí)進(jìn)行 RHS 查詢。講得更準(zhǔn)確一點(diǎn),RHS 查詢與簡(jiǎn)單地查找某個(gè)變量的值別無(wú)二致,而 LHS 查詢則是試圖找到變量的容器本身,從而可以對(duì)其賦值。從這個(gè)角度說(shuō),RHS 并不是真正意義上的“賦值操作的右側(cè)”,更準(zhǔn)確地說(shuō)是“非左側(cè)”。對(duì)于此處的“var a = 2;”變量a出現(xiàn)在左側(cè),所以是LHS查詢。

其實(shí)這樣說(shuō)很是籠統(tǒng),讀者對(duì)賦值的理解也可能局限于“=”的左右側(cè)而已。我們不妨換個(gè)角度來(lái)理解。還記得,前面我們說(shuō)到,變量最重要的兩點(diǎn)就是存儲(chǔ)和引用。那么代碼中出現(xiàn)變量時(shí),如果目的是要進(jìn)行存儲(chǔ),也就是我們關(guān)心的是要找到變量的容器本身,來(lái)進(jìn)行不同數(shù)據(jù)的存儲(chǔ)賦值操作,而不關(guān)心現(xiàn)在這個(gè)容器里面存的時(shí)候是什么,就會(huì)用到LHS。而如果我們的目的只是拿這個(gè)變量來(lái)用,也就是只關(guān)心這個(gè)變量存儲(chǔ)的內(nèi)容是啥,而不需要關(guān)心這個(gè)變量存在哪個(gè)容器,那么就會(huì)用到RHS。

4. 查詢與作用域鏈(作用域鏈會(huì)在今后詳細(xì)解讀)

事實(shí)上,查找的過(guò)程并不僅限于查找開(kāi)始時(shí)所處的當(dāng)前執(zhí)行作用域。當(dāng)一個(gè)塊或函數(shù)嵌套在另一個(gè)塊或函數(shù)中時(shí),就發(fā)生了作用域的嵌套。因此,在當(dāng)前作用域中無(wú)法找到某個(gè)變量時(shí),引擎就會(huì)在外層嵌套的作用域中繼續(xù)查找,直到找到該變量, 或抵達(dá)最外層的作用域(也就是全局作用域)為止。但是如果對(duì)變量的查詢?nèi)绻且圆檎也坏降慕Y(jié)果終止時(shí),LHS和RHS的表現(xiàn)是不同的。

  1. 如果 RHS 查詢?cè)谒星短椎淖饔糜蛑斜閷げ坏剿璧淖兞浚婢蜁?huì)拋出 ReferenceError 異常。
  2. 當(dāng)引擎執(zhí)行 LHS 查詢時(shí),如果在頂層(全局作用域)中也無(wú)法找到目標(biāo)變量,
    全局作用域中就會(huì)創(chuàng)建一個(gè)具有該名稱的變量,并將其返還給引擎,前提是程序運(yùn)行在非 “嚴(yán)格模式”下。在 嚴(yán)格模式中 LHS 查詢失敗時(shí),并不會(huì)創(chuàng)建并返回一個(gè)全局變量,引擎會(huì)拋出同 RHS 查詢 失敗時(shí)類似的 ReferenceError 異常。

看到這里,我們就能理解,為什么在函數(shù)內(nèi)部不用var 聲明變量而直接賦值時(shí),為什么該變量會(huì)成為一個(gè)全局變量的原因了。

如果 RHS 查詢找到了一個(gè)變量,但是你嘗試對(duì)這個(gè)變量的值進(jìn)行不合理的操作, 比如試圖對(duì)一個(gè)非函數(shù)類型的值進(jìn)行函數(shù)調(diào)用,或著引用 null 或 undefined 類型的值中的屬性,那么引擎會(huì)拋出另外一種類型的異常,叫作 TypeError。

ReferenceError 同作用域判別失敗相關(guān),而 TypeError 則代表作用域判別成功了,但是對(duì)結(jié)果的操作是非法或不合理的。

5. 詞法作用域與動(dòng)態(tài)作用域

我們來(lái)看一段代碼:

var value = 1;
function foo() {
    console.log(value);
}
function bar() {
    var value = 2;
    foo();
}
bar(); //打印出來(lái)的是什么?

在揭曉答案之前,我們來(lái)說(shuō)下作用域的兩個(gè)概念。

  1. 詞法作用域
    也叫靜態(tài)作用域,意味著作用域是由書寫代碼時(shí)函數(shù)聲明的位置來(lái)決定的。編譯的詞法分析階段基本能夠知道全部標(biāo)識(shí)符在哪里以及是如何聲明的,從而能夠預(yù)測(cè)在執(zhí)行過(guò)程中如何對(duì)它們進(jìn)行查找。
  2. 動(dòng)態(tài)作用域
    函數(shù)的作用域是在函數(shù)調(diào)用的時(shí)候才決定的。

那么,JavaScript到底采用的是哪種作用域呢?

  1. 假設(shè)JavaScript采用靜態(tài)作用域,讓我們分析下執(zhí)行過(guò)程:
    執(zhí)行 foo 函數(shù),先從 foo 函數(shù)內(nèi)部查找是否有局部變量 value,如果沒(méi)有,就根據(jù)書寫的位置,查找上面一層的代碼,也就是 value 等于 1,所以結(jié)果會(huì)打印 1。
  2. 假設(shè)JavaScript采用動(dòng)態(tài)作用域,讓我們分析下執(zhí)行過(guò)程:
    執(zhí)行 foo 函數(shù),依然是從 foo 函數(shù)內(nèi)部查找是否有局部變量 value。如果沒(méi)有,就從調(diào)用函數(shù)的作用域,也就是 bar 函數(shù)內(nèi)部查找 value 變量,所以結(jié)果會(huì)打印 2。

事實(shí)上,JavaScript采用的是詞法作用域,所以這個(gè)例子的結(jié)果是 1。

參考

JavaScript深入之詞法作用域和動(dòng)態(tài)作用域
JS入門難點(diǎn)解析2-JS的變量提升和函數(shù)提升
BOOK-《JavaScript高級(jí)程序設(shè)計(jì)(第3版)》
BOOK-《你不知道的JavaScript》 第1部分

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容