什么是作用域
什么是作用域?
根据名称查找变量的一套规则
JS 是动态(代码在执行的时候才会进行检查)的弱类型(隐式类型转换)
程序在执行之前一般会经历三个步骤:
词法分析
将由字符组成的字符串分解成有意义的代码块,这些代码块被称为词法单元(token)
语法分析
根据词法单元生成抽象语法树
- 代码生成
将 AST 转换为可执行的代码
代码在执行前都会进行编译。
理解作用域
有几个关键的人物
引擎:编译和执行 JS 代码
编译器:负责语法分析和代码生成
作用域:负责收集并维护所有声明的标识符,以及管理代码对标识符的访问权限(变量的访问规则)
例如对于var a = 2;
这段代码:
编译器会在当前作用域中声明一个变量 a(如果之前没有声明过)
运行时引擎会在作用域中查找 a,如果能找到则会对它进行赋值
查找一个变量的过程就需要作用域协助,但是引擎怎样查找会影响最终的查找结果
两种查找类型:LHS 和 RHS
一个 L 和一个 R,分别代表左侧和右侧,那么是什么东西的左侧和右侧呢?
赋值操作
换句话说,当变量出现在赋值运算符的左侧时进行 LHS 查询,出现在右侧是进行 RHS 查询
- LHS
试图找到变量本身的容器,从而可以对其赋值
- RHS
找到某个变量的值(所以 RHS 不是真正意义上的赋值操作的右侧,而是非左侧)
比如:
console.log(a);
此时对于变量 a 进行的就是 RHS 查询,查找变量 a 的值
相应的
a = 2;
此时对于 a 就是 LHS 查询,因为我们不关心 a 的值是什么,只是想要为 = 2
找到一个容器
LHS 和 RHS 的小测验
function foo(a) {
var b = a;
return a + b;
}
var c = foo(2);
- 找出其中的 LHS 查询
c (foo(2) 的结果赋值给 c)
a (2 赋值给 a)
b (a 赋值给 b)
- 找出其中的 RHS 查询
foo 调用
a (a 赋值给 b)
(a + b) 中的 a 和 b
作用域嵌套
作用域嵌套的时候,当前作用域中查找不到某个变量的时候就会向外层作用域中继续查找,直到找到这个变量或者是抵达到全局作用域为止。
作用域链就是作用域会发生嵌套,当作用域嵌套时,当前作用域中查找不到某个变量的时候就会向外层作用域中继续查找,直到找到这个变量或者是抵达到全局作用域为止。这样就形成了作用域链
异常
为什么区分 LHS 和 RHS 很重要?
当一个变量没有声明,即在任何作用域下都找不到这个变量的时候,两种查询的行为是不一样的。
- 找不到变量时
对于一个在任何作用域下都没有声明的变量进行 RHS 查找的时候,找不到就会报 ReferenceError;
对于一个在任何作用域下都没有声明的变量进行 LHS 查找的时候,找不到就会在全局作用域下声明该变量,前提是在非严格模式下(严格模式下,不会隐式创建全局变量,LHS 找不到的时候也会报 ReferenceError)
- 找到了但是进行一些不合理的操作
- 比如说,RHS 找到了一个变量,但你尝试对这个变量的值进行一些不合理的操作,例如访问 null/undefined 上的属性或者方法,对一个非函数类型的值进行函数调用,此时就会报 TypeError
总结
作用域是查找变量的一套规则。
如果查找的目的是对变量进行赋值操作,那么就是 LHS 查询;如果查找的目的是使用变量的值,那么就是 RHS 查找
嵌套的作用域会形成作用域链,在当前作用域中查找不到的时候就会向外层作用域查找,直到找到变量或者是到全局作用域为止
RHS 查找失败会导致 ReferenceError,LHS 查找失败在非严格模式下会隐式在全局作用域中声明变量严格模式下抛 ReferenceError
RHS 查找到某个变量的时候,对其进行不合法操作会导致 TypeError