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
属性的访问!