從LHS和RHS角度理解JavaScript的變量引用

1.理解JavaScript中的LHS 和 RHS 查詢。

JavaScript中在預(yù)編譯后執(zhí)行代碼時(shí)對(duì)變量的查詢分為LHS(Left-Hand-Side)查詢和RHS(Right-Hand-Side)查詢。

當(dāng)你看到 var a = 2; 這段程序的時(shí)候,很可能認(rèn)為這是一句聲明,但是瀏覽器的引擎并不這么看,事實(shí)上引擎認(rèn)為這里有兩個(gè)完全不同的聲明,一個(gè)由編譯器在編譯時(shí)候處理,另一個(gè)則是在引擎運(yùn)行時(shí)處理。

下面我們將 var a = 2; 分解,看看引擎和它的朋友們是如何協(xié)同工作的。

編譯器首先會(huì)將這段程序分解成詞法單元,然后將詞法單元解析成一個(gè)樹結(jié)構(gòu),但是當(dāng)編譯器開始進(jìn)行代碼生成的時(shí)候,它對(duì)這段程序的處理方式會(huì)和逾期的有所不同。

可以合理的假設(shè)編譯器所產(chǎn)生的代碼能夠用下面的偽代碼進(jìn)行概括:“為一個(gè)變量分配內(nèi)存,將其命名為a,然后將值2保存進(jìn)這個(gè)變量”然而,這并不完全正確。

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

  • 1.遇到 var a,編譯器會(huì)詢問作用域是否已經(jīng)有一個(gè)該名稱的變量存在于同一個(gè)作用域的集合中,如果是編譯器會(huì)自動(dòng)忽略該聲明繼續(xù)進(jìn)行編譯,否則它會(huì)要求作用域在當(dāng)前作用域的集合中聲明一個(gè)新的變量,并命名為a。

  • 2.接下來編譯器會(huì)為引擎生成運(yùn)行時(shí)所需要的代碼,這些代碼被用來處理 a=2 這個(gè)賦值操作引擎運(yùn)行時(shí)會(huì)首先詢問作用域在當(dāng)前的作用域集合中是否存在一個(gè)叫做a的變量。如果是,引擎就會(huì)使用這個(gè)變量;如果不是,引擎會(huì)繼續(xù)查找該變量

如果引擎最終找到了a變量,就會(huì)將2賦值給它,否則引擎就會(huì)舉手示意拋出一個(gè)異常

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

為了進(jìn)一步理解,我們需要多介紹一些編譯器的術(shù)語。

編譯器在編譯過程中的第二步中生成代碼,引擎執(zhí)行它時(shí),會(huì)通過查找變量a來判斷它時(shí)否已經(jīng)聲明過,查找的過程由

當(dāng)變量出現(xiàn)在賦值操作的左側(cè)時(shí)進(jìn)行 LHS 查詢,出現(xiàn)在右側(cè)時(shí)進(jìn)行 RHS 查詢。

講個(gè)更加清楚一些,RHS查詢與簡單地查找某一個(gè)變量的值別無二致,而LHS查詢則是試圖找到變量本身容器本身,從而可以對(duì)其賦值,從這個(gè)角度來說,RHS并不是真正意義上的“賦值操作的右側(cè)”,更準(zhǔn)確地說是“非左側(cè)”。

你可以將RHS理解成 retrueve his source value,這意味著“得到某某的值”

讓我們繼續(xù)深入研究。

考慮以下代碼:

onsole.log(a);

其中對(duì) a 的引用是一個(gè)RHS引用,因?yàn)檫@里a并沒有賦予任何值。相應(yīng)地,需要查找并取得a的值,這樣才能將值傳遞給console.log(...)。

相比之下,例如:

a = 2;

這里對(duì)ade 引用則是LHS的引用,因?yàn)閷?shí)際上我們并不關(guān)心當(dāng)前的值是什么,知識(shí)想要為 = 2 這個(gè)賦值操作找到一個(gè)目標(biāo)。

考慮下面的程序,其中既有LHS也有RHS引用:

  function(a){
    console.log(a); // 2
  }
  foo(2);

最后一行 foo(...) 函數(shù)的調(diào)用需要對(duì)foo進(jìn)行RHS引用,意味著"去找到foo的值",并把它給我"。并且(...)意味著foo的值需要被執(zhí)行,因此它最好真的是一個(gè)函數(shù)類型的值!

這里還有一個(gè)容易被忽略但確實(shí)非常重要的細(xì)節(jié)。

代碼中隱式的 a = 2 操作很可能被忽略掉,這個(gè)操作發(fā)生在 2 被當(dāng)做參數(shù)傳遞給 foo(...) 函數(shù)的時(shí)候,2會(huì)被分配給參數(shù)a ,為了給參數(shù)a 分配值,需要進(jìn)行一次LHS查詢。

這里還有對(duì)a 進(jìn)行的RHS引用,并且將得到的值傳遞給了 console.log(..) console.log(..) 本身也需要一個(gè)引用才能執(zhí)行,因此會(huì)對(duì)console
對(duì)象進(jìn)行RHS查詢 并且檢查得到的值中是否有一個(gè)叫做log的方法。

2.作用域的嵌套

我們說過,作用域是根據(jù)名稱查找變量的一套規(guī)則,實(shí)際情況中,通常需要同時(shí)顧忌幾個(gè)作用域。

當(dāng)一個(gè)塊或者函數(shù)嵌套在另一個(gè)塊或者函數(shù)中的時(shí)候,就發(fā)生了作用域的嵌套,因此,在當(dāng)前作用域中無法找到某一個(gè)變量時(shí)候,引擎就會(huì)在外層嵌套的作用域中繼續(xù)查找,直到找到該變量,或者抵達(dá)最外層的作用域(也就是全局的作用域)為止。

考慮一下代碼:

  function foo(a) {
    console.log(a+b);
  }

  var b = 2;
  foo( 2 ); // 4s

對(duì)b進(jìn)行的 RHS 引用無法在 函數(shù) foo 內(nèi)部完成,但是可以在上一級(jí)作用域(在這個(gè)例子中就是全局作用域)中完成。

遍歷嵌套作用域鏈的規(guī)則很簡單:引擎從當(dāng)前的執(zhí)行作用域開始查找變量,如果找不到就向上一級(jí)繼續(xù)查找,當(dāng)?shù)诌_(dá)最外層的全局作用域時(shí)候,無論找到還是沒有找到 查找過程都會(huì)停止。

3.區(qū)分LHS和RHS是一件重要的事情

在變量還沒有聲明(在任何的作用域中都無法找到該變量)的情況下,這兩種查詢的行為是不一樣的。

考慮如下代碼:

  function foo(a) {
    console.log(a+b);
    b = a;
  }
  foo(2);

第一次 對(duì) b 進(jìn)行RHS 查詢時(shí)無法找到該變量的。也就是說,這是一個(gè)”未聲明“的變量,因?yàn)樵谌魏蜗嚓P(guān)的作用域中都無法找到它。

如果RHS查詢?cè)谒星短椎淖饔糜蛑斜閷げ坏剿枰淖兞?,引擎就?huì)拋出 ReferenceError 異常,值得注意的是,ReferenceError 是非常重要的異常類型。

相比較之下,當(dāng)引擎執(zhí)行LHS 查詢的時(shí)候,如果在頂層(全局作用域)中無法找到目標(biāo)變量,全局作用域中就會(huì)創(chuàng)建一個(gè)具有該名稱的變量,并將其返還給引擎,前提是程序運(yùn)行在非”嚴(yán)格模式“下。

ES5中引入了”嚴(yán)格模式”。同正常模式相比有很多的不同,其中一個(gè)不同是嚴(yán)格模式禁止自動(dòng)或者隱式創(chuàng)建全局變量,因此在嚴(yán)格模式中LHS查詢失敗的時(shí)候,并不會(huì)創(chuàng)建并返回一個(gè)全局變量,引擎會(huì)拋出同RHS查詢的失敗類似的 ReferenceError 異常。

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

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

4.總結(jié)一下

作用域是一套規(guī)則,用于確定在何處以及如何查找變量(標(biāo)識(shí)符)如果查找的目的是對(duì)變量進(jìn)行賦值,那么就會(huì)使用LHS查詢,如果目的是獲取變量的值,就會(huì)使用RHS進(jìn)行查詢。

=操作符號(hào)或者調(diào)用函數(shù)時(shí)候傳入的參數(shù)操作都會(huì)導(dǎo)致關(guān)聯(lián)作用域的賦值操作。

Javascript 引擎首先會(huì)在代碼執(zhí)行前對(duì)其進(jìn)行編譯,在這個(gè)過程中,像 var a = 2 這樣的聲明會(huì)被分解為兩個(gè)獨(dú)立的步驟:
1.首先 var a 在其作用域中聲明新的變量,這會(huì)在最開始的階段,也就是在代碼執(zhí)行前進(jìn)行。

2.接下來 a = 2 會(huì)在查詢(LHS)變量a并對(duì)其進(jìn)行賦值。

LHS和RHS查詢都會(huì)從當(dāng)前的作用域中開始,如果有需要,就會(huì)向上級(jí)作用域繼續(xù)查找目標(biāo),這樣的層級(jí)查找直到抵達(dá)全局的作用域,無論找到或沒有找到都會(huì)停止。

不成功的 RHS 引用會(huì)導(dǎo)致拋出 ReferenceError 異常。不成功的 LHS 引用會(huì)導(dǎo)致自動(dòng)隱式 地創(chuàng)建一個(gè)全局變量(非嚴(yán)格模式下),該變量使用 LHS 引用的目標(biāo)作為標(biāo)識(shí)符,或者拋 出 ReferenceError 異常(嚴(yán)格模式下)。

5.鞏固知識(shí)

通過解析下面這段代碼復(fù)習(xí)一遍剛才理解和總結(jié)的LHS和RHS的概念。

function foo(a) { 
  var b = a;
  return a + b; 
}

var c = foo( 2 );

LHS: 3處

1.首先在執(zhí)行 var c = foo(2) 這段代碼的時(shí)候 變量 c 的賦值就是一個(gè) LHS操作
2.這里foo函數(shù)執(zhí)行了一個(gè)傳遞參數(shù)的行為,上文提到這種行為屬于隱式類型分配--> a = 2 屬于LHS
3.執(zhí)行到 foo函數(shù)內(nèi)部 var b = a; 給b賦值的操作是一個(gè)LHS行為

RHS:4處

1.var c = foo(2) 這段代碼 中對(duì)于 foo函數(shù)的執(zhí)行就是一個(gè)RHS的查詢操作。
2.執(zhí)行到 foo函數(shù)內(nèi)部 var b = a; 對(duì)a 變量的查找的行為就是一個(gè)RHS查詢操作。
3.return a + b; 這段代碼 執(zhí)行的時(shí)候 分別查詢了 a 和 b

參考資料 《你不知道的JavaScript》上卷

最后編輯于
?著作權(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),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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