1. 程式人生 > >從LHS和RHS角度理解JavaScript的變數引用

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

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

JavaScript中在預編譯後執行程式碼時對變數的查詢分為LHS(Left-Hand-Side)查詢和RHS(Right-Hand-Side)查詢。

當你看到 var a = 2;這段程式的時候,很可能認為這是一句宣告,但是瀏覽器的引擎並不這麼看,事實上引擎認為這裡有兩個完全不同的宣告,一個由編譯器在編譯時候處理,另一個則是在引擎執行時處理。

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

編譯器首先會將這段程式分解成詞法單元,然後將詞法單元解析成一個樹結構,但是當編譯器開始進行程式碼生成的時候,它對這段程式的處理方式會和逾期的有所不同。

可以合理的假設編譯器所產生的程式碼能夠用下面的虛擬碼進行概括:“為一個變數分配記憶體,將其命名為a,然後將值2儲存進這個變數”然而,這並不完全正確。

事實上編譯器會進行如下的處理:

  • 1.遇到 var a,編譯器會詢問作用域是否已經有一個該名稱的變數存在於同一個作用域的集合中,如果是編譯器會自動忽略該宣告繼續進行編譯,否則它會要求作用域在當前作用域的集合中宣告一個新的變數,並命名為a。

  • 2.接下來編譯器會為引擎生成執行時所需要的程式碼,這些程式碼被用來處理 a=2 這個賦值操作引擎執行時會首先詢問作用域在當前的作用域集合中是否存在一個叫做a的變數。如果是,引擎就會使用這個變數;如果不是,引擎會繼續查詢該變數

如果引擎最終找到了a變數,就會將2賦值給它,否則引擎就會舉手示意丟擲一個異常

總結:變數的賦值操作會執行兩個動作,首先編譯器會在當前作用域中宣告一個變數,然後再執行時引擎會在作用域中查詢該變數,如果能夠找到就會對它進行賦值。

為了進一步理解,我們需要多介紹一些編譯器的術語。

編譯器在編譯過程中的第二步中生成程式碼,引擎執行它時,會通過查詢變數a來判斷它時否已經宣告過,查詢的過程由

當變量出現在賦值操作的左側時進行 LHS 查詢,出現在右側時進行 RHS 查詢。

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

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

讓我們繼續深入研究。

考慮以下程式碼:

onsole.log(a);

其中對 a 的引用是一個RHS引用,因為這裡a並沒有賦予任何值。相應地,需要查詢並取得a的值,這樣才能將值傳遞給console.log(...)

相比之下,例如:

a = 2;

這裡對ade 引用則是LHS的引用,因為實際上我們並不關心當前的值是什麼,知識想要為 = 2 這個賦值操作找到一個目標。

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

  function(a){
    console.log(a); // 2
  }
  foo(2);
複製程式碼

最後一行 foo(...) 函式的呼叫需要對foo進行RHS引用,意味著"去找到foo的值",並把它給我"。並且(...)意味著foo的值需要被執行,因此它最好真的是一個函式型別的值!

這裡還有一個容易被忽略但確實非常重要的細節。

程式碼中隱式的 a = 2 操作很可能被忽略掉,這個操作發生在 2 被當做引數傳遞給 foo(...) 函式的時候,2會被分配給引數a ,為了給引數a 分配值,需要進行一次LHS查詢。

這裡還有對a 進行的RHS引用,並且將得到的值傳遞給了 console.log(..) console.log(..) 本身也需要一個引用才能執行,因此會對console 物件進行RHS查詢 並且檢查得到的值中是否有一個叫做log的方法。

2.作用域的巢狀

我們說過,作用域是根據名稱查詢變數的一套規則,實際情況中,通常需要同時顧忌幾個作用域。

當一個塊或者函式巢狀在另一個塊或者函式中的時候,就發生了作用域的巢狀,因此,在當前作用域中無法找到某一個變數時候,引擎就會在外層巢狀的作用域中繼續查詢,直到找到該變數,或者抵達最外層的作用域(也就是全域性的作用域)為止。

考慮一下程式碼:

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

  var b = 2;
  foo( 2 ); // 4s
複製程式碼

對b進行的 RHS 引用無法在 函式 foo 內部完成,但是可以在上一級作用域(在這個例子中就是全域性作用域)中完成。

遍歷巢狀作用域鏈的規則很簡單:引擎從當前的執行作用域開始查詢變數,如果找不到就向上一級繼續查詢,當抵達最外層的全域性作用域時候,無論找到還是沒有找到 查詢過程都會停止。

3.區分LHS和RHS是一件重要的事情

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

考慮如下程式碼:

  function foo(a) {
    console.log(a+b);
    b = a;
  }
  foo(2);
複製程式碼

第一次 對 b 進行RHS 查詢時無法找到該變數的。也就是說,這是一個”未宣告“的變數,因為在任何相關的作用域中都無法找到它。

如果RHS查詢在所有巢狀的作用域中遍尋不到所需要的變數,引擎就會丟擲 ReferenceError 異常,值得注意的是,ReferenceError 是非常重要的異常型別。

相比較之下,當引擎執行LHS 查詢的時候,如果在頂層(全域性作用域)中無法找到目標變數,全域性作用域中就會建立一個具有該名稱的變數,並將其返還給引擎,前提是程式執行在非”嚴格模式“下。

ES5中引入了”嚴格模式”。同正常模式相比有很多的不同,其中一個不同是嚴格模式禁止自動或者隱式建立全域性變數,因此在嚴格模式中LHS查詢失敗的時候,並不會建立並返回一個全域性變數,引擎會丟擲同RHS查詢的失敗類似的 ReferenceError 異常。

接下來,如果RHS 查詢找到了一個變數,但是你嘗試對這個變數的值進行不合理的操作,比如對於一個非函式型別的值進行函式的呼叫,或者引用null 或者undefined 型別的值中的屬性,那麼引擎就會丟擲另外一種型別的異常,叫做TypeError

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

4.總結一下

作用域是一套規則,用於確定在何處以及如何查詢變數(識別符號)如果查詢的目的是對變數進行賦值,那麼就會使用LHS查詢,如果目的是獲取變數的值,就會使用RHS進行查詢。

=操作符號或者呼叫函式時候傳入的引數操作都會導致關聯作用域的賦值操作。

Javascript 引擎首先會在程式碼執行前對其進行編譯,在這個過程中,像 var a = 2 這樣的宣告會被分解為兩個獨立的步驟: 1.首先 var a 在其作用域中宣告新的變數,這會在最開始的階段,也就是在程式碼執行前進行。

2.接下來 a = 2 會在查詢(LHS)變數a並對其進行賦值。

LHS和RHS查詢都會從當前的作用域中開始,如果有需要,就會向上級作用域繼續查詢目標,這樣的層級查詢直到抵達全域性的作用域,無論找到或沒有找到都會停止。

不成功的 RHS 引用會導致丟擲 ReferenceError 異常。不成功的 LHS 引用會導致自動隱式 地建立一個全域性變數(非嚴格模式下),該變數使用 LHS 引用的目標作為識別符號,或者拋 出 ReferenceError 異常(嚴格模式下)。

5.鞏固知識

通過解析下面這段程式碼複習一遍剛才理解和總結的LHS和RHS的概念。

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

var c = foo( 2 );
複製程式碼

LHS: 3處

1.首先在執行 var c = foo(2) 這段程式碼的時候 變數 c 的賦值就是一個 LHS操作
2.這裡foo函式執行了一個傳遞引數的行為,上文提到這種行為屬於隱式型別分配--> a = 2 屬於LHS 3.執行到 foo函式內部 var b = a; 給b賦值的操作是一個LHS行為

RHS:4處

1.var c = foo(2) 這段程式碼 中對於 foo函式的執行就是一個RHS的查詢操作。 2.執行到 foo函式內部 var b = a; 對a 變數的查詢的行為就是一個RHS查詢操作。 3.return a + b; 這段程式碼 執行的時候 分別查詢了 a 和 b

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