LHS和RHS
之前我們先來回憶一下最簡單的賦值操作!
var?test=100; console.log(test);
以上代碼的意思簡單我們理解為把右邊
的值
賦值給左邊
的test變量
,然后輸出打印結果。
可是我們要是深入理解你就會發現在這個過程當中,還發生了一些其他的事情
而這些事情就是今天我們要說的LHS和RHS
就比如之前那段代碼:
var test=100;
JS
會將其看成兩句聲明:var test
和 test=100
弄成兩個部分: 編譯
和代碼執行
var test
這個部分也就是定義聲明
的時候就開始進行編譯(編譯器)
test=100
而后面這一段賦值聲明
會留在原地等待代碼執行(引擎)
那么JS
中的變量賦值操作
會被拆分執行為兩個動作:
-
JS編譯器
會在當前作用域
中聲明這個變量
,當然是這個變量
不存在的情況下! -
然后在運行代碼時,
JS引擎
會在作用域
中查找
這個變量
,如果能找到就會對它進行一些操作,例如:賦值操作
而以上這兩部的操作又間接引出LHS和RHS
的概念!
所以這里的JS編譯
又與傳統的編譯語言
是不同的!
LHS與RHS基本概念
LHS 全稱為: Left-hand Side(左側引用)
LHS
其實就是賦值操作
的左側
查詢,LHS查詢
試圖找到變量
的容器本身,從而對其賦值!
注意:
=操作符
或調用函數
時傳入參數的操作都會導致賦值操作!
小結
通常情況下,如果查找的目的是對變量
進行賦值,那么就會使用LHS查詢 也就是當變量
出現在賦值操作
的左側
RHS 全稱為: Right-hand Side(右側引用)
RHS
其實就是賦值操作
的右側
查詢,可以理解為需要獲取到某值
小結
通常情況下,如果查找的目的是獲取變量或函數
的值,就會使用RHS查詢, 也就是當變量
出現在賦值操作
的右側
總的來說LHS和RHS
通常是指等號賦值運算
的時候,左右邊的引用!
可能這樣說對于理解LHS和RHS
還是比較抽象
,我們要用一個案例來解釋一下!
舉個梨子
console.log(test);
按照LHS與RHS
的查找規范, 這段代碼就是一個所謂的RHS右側引用
因為這里test變量
,我們并沒有對其進行賦值操作,而只是想在作用域
當中查找這個變量
并取得值,然后輸出打印,所以執行的就是RHS
test = 100;
而這段代碼當中 按照LHS與RHS
的查找規范, 就是一個LHS左側引用
因為此時JS
并不關心當前的值是什么, 只是想要給當前這個賦值操作
找到一個目標容器!
我們再看一個案例, 大家可以猜猜看以下代碼當中有多少個LHS查詢
又有多少RHS查詢
?
代碼如下
function?foo(num) { console.log(num); }foo(100);
分析
-
首先
function foo(num)
這里就存在一個LHS查詢
因為100
賦值給了num形參
對吧,也就相當于num=100
,那么就會在這個函數作用域當中先找出num
這個變量容器
! -
而
foo(100)
本身就是在求返回值
的操作, 在作用域當中就會進行RHS查找
這個foo(100)函數
是否存在, 并沒有對其進行賦值操作, 所以這里很明顯就是一個RHS查找
-
最后
console.log(num)
這里其實又是一個RHS查詢
, 因為在這行代碼中,我們只有一個變量num
被使用,因此在console.log(num)
中只有一個RHS查詢
,在作用域中查找, 用于獲取num
的值!
所以正確答案是以上代碼當中存在1個LHS查詢
, 2個RHS查詢
所以通過上面的案例最后我們可以把LHS與RHS
簡單的理解為以下概念:
賦值操作的目標是誰,那么就是LHS(左側引用查找),
誰是賦值操作的源頭,則就是RHS(右側引用查找)
面試題: 請找出以下代碼當中,所有的LHS和所有的RHS
function?test(num) { var?num2?=?num; return?num?+?num2; } var?num?=?test(100);
代碼分析
包含LHS的代碼
-
var num = test(100)
這一段代碼很顯然是一個LHS
,因為num
在賦值運算的左邊
,也就是賦值操作的目標
,所以要對num變量
進行LHS查詢
, 那么這里的查詢
過程就是由作用域
(詞法作用域)進行配合查找的! -
function test(num)
的形參num
在調用test(100)
時,將實參100賦值給了形參num
,也就是num=100
,因為形參num
在賦值運算的左邊
,也就是賦值操作的目標
,所以要對形參num
進行LHS查詢
-
var num2 = num
這一段代碼也是一個LHS
,因為num2
在賦值運算的左邊
,也就是賦值操作的目標
,所以要對num2
進行LHS查詢
包含RHS的代碼
-
var num = test(100)
這段代碼雖然有LHS查詢
但同時也有RHS
原因之前其實我們也說過,可以把這段代碼分成兩個部分來看,其中就有test(100)
調用這部分,而這里恰恰是要求得一個結果,需要知道test(100)
的值是多少 那么根據誰是賦值操作的源頭
是誰則就是RHS
,從而獲取test(100)
的返回值 -
var num2 = num
也跟上面同理雖然有LHS查詢
但同時也有RHS
,也就是其中也有num
這個變量
在賦值運算符右邊
, 此時需要知道num
的值 那么根據誰是賦值操作的源頭
是誰則就是RHS
-
return num + num2
這里我們按照(詞法作用域查找)
思維來說需要知道num
和num2
的值, 也就是說只是想查找這兩個變量,
并取得它們的值,然后才是進行求和
操作, 所以這里就是RHS查找
, 也就是需要分別對num 和num2
都進行RHS查詢
那么根據上面的案例當中,要看出
左側
和右側
并不一定意味這就是=號
的左側和右側,賦值操作還有其他幾種形式
小結
如果查找的目的是對變量進行賦值,那么就會使用LHS查詢 如果目的是獲取變量的值,就會使用RHS查詢
LHS與RHS查找規則
從之前的案例當中,不管是LHS
還是RHS
都會在當前執行的作用域
中開始查找變量
LHS
在查找的時候,是把右邊
的值
賦值給左邊
的變量
那么就會對左邊
的變量
進行當前作用域
中的LHS查詢
,來判斷是否聲明過!
RHS
在查找的時候,是先看誰是賦值操作的源頭
, 然后在這個基礎之上進行當前作用域
中的RHS查詢
,簡單點說也就是在當前作用域
中查找右邊
的變量
或者函數表達式
來判斷是否已經聲明過!
那么LHS和RHS
在當前作用域
當中如果沒有找到所需的標識符
就會根據作用域鏈
向上一級作用域
繼續查找該標識符
,以此類推這樣每次上升一層作用域
去查找, 最后到達全局作用域
就會停止,這也是我們之前講過的作用域鏈
!
我們可以回顧一下之前的作用域鏈
如圖
LHS
和RHS
都會在當前執行代碼的所在環境
進行查找, 如果沒有找到,才往上一層查找 以此類推, 一旦抵達頂層全局作用域
之后,可能找到了你所需的變量
,也可能沒找到,但無論如何查找過程都將停止!
結合(作用域+編譯器+JS引擎)來理解LHS和RHS
我們用一段代碼來說明:
var test = 100;
分析
JS引擎
會認為這里有兩個完全不同的聲明
一個由編譯器
在編譯時處理也就是var test
另一個則由JS引擎
在運行時處理,也就是test = 100
當編譯器
遇到var test
會向當前作用域
詢問是否已經存在這個變量
, 如果有就交給編譯器
繼續進行編譯賦值, 否則它會要求作用域
在當前作用域
的中聲明一個新的變量test
然后JS引擎
在運行代碼
的時候,會首先詢問作用域
,在當前的作用域
中是否存在一個叫作test
的變量
, 如果有,JS引擎
就會使用這個變量
, 如果沒有JS引擎
會繼續根據作用域鏈
查找該變量
如果JS引擎
最終找到了test變量
,就會將100賦值給它, 否則JS引擎
就會拋出一個異常!
LHS與RHS異常問題
當RHS查詢當前作用域
中如果找不到變量
,引擎會拋出ReferenceError錯誤
例如: 直接在整個作用域
當中打印輸出
一個沒有定義聲明
的變量或函數
就會報ReferenceError錯誤
如圖
例如:
代碼如下
function foo(num) { console.log(num + data); data = num;}foo( 100);
以上的代碼當中進行了RHS
但無法查找到, 因為作用域
是往上查找的,這里也很明顯是找不到的!
如圖
函數也是一樣, 在調用一個完全沒有聲明
的函數
時也會拋出ReferenceError錯誤
就比如說如下代碼:
test();
如圖
所以在作用域
中直接調用一個沒有定義的test()函數
直接在整個作用域
進行了RHS
查找也沒有查到
自然會報ReferenceError錯誤
但是如果RHS在作用域
中查找到變量
,但是進行了不規范的操作, 比如: 在末尾加了個()
簡單點說就是試圖對一個非函數類型
的值/變量
進行函數調用
那么JS引擎
會拋出另一種異常
叫做 TypeError(類型異常)
如圖
還有就是如果RHS在作用域
中查找到變量
, 但是引用了null
和 undefined
類型的值中的屬性,也會報一個TypeError(類型異常)
的錯誤!
知識點復習
在 JS 中,null 和 undefined 是特殊的值,用于表示變量或屬性不存在或沒有值。如果你引用 null 或 undefined 類型的值中的屬性,意味著你嘗試訪問一個不存在的屬性或者一個沒有被賦值的變量。具體而言,如果你嘗試在一個 null 或 undefined 類型的值中訪問一個屬性,JS引擎會拋出一個類型錯誤(TypeError),提示你不能在 null 或 undefined 上訪問屬性。
例如,以下代碼會拋出TypeError(類型異常)
!
var obj=null;obj.username;
如圖
所以這里總結以下:
ReferenceError 是作用域
查找失敗,也就是說找不到這個變量或者函數
才拋出來的
而TypeError則代表作用域
查找成功了, 但是對結果
的操作是非法
或不合理
的!
當JS引擎
執行LHS查詢時,如果在所有作用域
中找不到目標變量
,就會在全局作用域
中創建一個與該變量名
相同的全局變量,并將其返回給JS引擎
,前提是在非嚴格模式下
這也正好呼應了我們前面所講解的隱式全局變量的真正含義!
代碼分析以下函數當中的a = 100其實是一個LHS,但是變量a并沒有在函數塊當中進行聲明就直接賦值了,那么沒聲明的變量正常情況下,作用域中是找不到的那么LHS則會在"全局作用域"中創建一個與該"變量"名相同的"全局變量a"
如圖
我們再看一段代碼
function foo(a){ num = a; //num = 100}foo(100);
分析
上面的代碼執行的LHS查詢
,在非嚴格模式
下,JS引擎
直到在全局作用域
中都沒有找到num
這個變量,所以它就在全局作用域中
聲明了一個變量num
當然也對變量a
進行一次RHS查詢
以獲得變量a
的值, 所以此時結果不會報錯, 并且num
也被賦值為100
我們也可以使用Chrome
調試工具當中的Sources
中斷點
來查看全局作用域
當中是否真的存在這個num變量
,果不其然,我們在global
中找到了這個num變量
如圖
但是如果是嚴格模式
下會ReferenceError的錯誤
代碼如下:
"use strict";function test(){ a=100;}test();console.log(a);
如圖
也就是說在嚴格模式下,LHS查詢
是無法幫助我們建立隱式全局變量
的
LHS與RHS的區別
其實我們在熟悉了LHS和RHS
拋出的異常問題之后,就會明白它們彼此的區別在什么地方了!
RHS查詢
在所有嵌套
的作用域
中找不到所需的變量或函數
,引擎就會拋出ReferenceError
異常
但是要注意的是,如果RHS查詢
找到了一個變量
,但是對這個變量
的值進行不合理的操作, 例如: 使用null
或者undefnied
類型的屬性,這種違規操作, JS引擎
會拋出TypeError異常
LHS查詢
相比之下,非嚴格模式
的情況下 執行LHS查詢
時,如果在頂層作用域
也無法找到目標變量
,那么全局作用域
會創建一個具有該名稱的隱式全局變量
,并將其返回給JS引擎
, 當然如果是在嚴格模式
下,LHS查詢
找不到目標變量
時, 依舊會拋出ReferenceError異常
總結
所以區分LHS和RHS
很重要的依據就是最終 查詢在作用域鏈
中找不到需要的變量
或函數
會拋出什么!
而LHS和RHS
都會在當前執行作用域
中開始查詢,當前沒找到,就會根據作用域鏈
向上級作用域
繼續查找目標標識符
不成功的RHS
會導致拋出ReferenceError異常
不成功的LHS
會自動隱式
在全局作用域
中創建一個同名的全局變量
嚴格模式
下也會拋出ReferenceError
異常
作用域與LHS和RHS之間的關系
之前我們學過作用域
是JS引擎
用來管理如何在當前作用域
以及嵌套的子作用域
中根據標識符
名稱進行變量
查找的一套規則
如果查找的目的是對變量進行賦值
,那么就會使用LHS查詢
如果目的是獲取變量的值,就會使用RHS查詢
小提示:
要注意一點: 如果代碼中引用了類似于foo.bar.baz
,那么詞法作用域
查找只會試圖查找foo標識符
,找到這個變量
后,再根據對象屬性訪問
規則會分別接管, 并對bar
和baz
屬性的訪問!