作用域是什么
前言: 本章对js的编译原理,js变量的查找方式,作用域,引擎,编译器之间的协作进行介绍
什么是作用域?
js中存储变量,查找变量的规则就是作用域
编译原理
js事实上也是一门编译语言,大部分情况下编译发生在执行前的几微秒
什么是编译呢? 编译就是将js代码转换成机器指令的过程,js中这个过程包含三个步骤:
-
分词/词法分析
这个过程会将源代码字符串转换成一个个
词法单元,如var a = 2;这句代码会转换成五个词法单元:var,a,=,2,; -
解析/语法分析
这个过程会将词法分析产生的
词法单元流构建为抽象语法树(AST),抽象语法树就是由元素逐级嵌套组成的代表程序语法结构的树var a = 2;的AST抽象语法树如下所示,推荐一个生成AST的网站: astexplorer
-
代码生成
这个过程会将AST转换成可执行的机器指令: 用来创建一个叫做a的变量,并将一个值存储在a中
引擎 编译器和作用域
实际上在编译执行的过程中情况还要复杂的多,主要由以下三个部分协同合作:
- 引擎: 主要负责执行过程
- 编译器: 主要负责词法分析,语法分析
- 作用域: 主要负责收集并维护由所有标识符组成的一些列查询,并实施一套严格的规则,确定当前执行代码对标识符的访问权限
以var a = 2;为例看其中是如何协作的:
传统思想中我们对这句代码的解释大概是: 为变量分配一个内存,将其命名为a,然后将2保存到这个变量.实时上处理过程要复杂的多:
- 编译器解析
var a,先向作用域询问同一作用域下的集合中是否声明的有这个名称的变量,如果有: 编译器会忽略该声明,继续编译,如果没有: 编译器会让作用域在当前作用域集合中声明该变量,之后编译器继续编译,并将编译结果给引擎进行执行 - 引擎拿到
a = 2,开始执行时会向询问作用域当前作用域集合中是否声明了该变量,如果有,那么引擎会使用该变量,如果没有那么引擎会继续查找,直到找不到,就会抛出错误
LHS和RHS
在上述过程中,引擎会查找变量a,如何查找变量,会影响到最终的查找结果.共有两种查找方式: LHS,RHS
LHS: 赋值操作左侧查询,对a的LHS可以理解为: 赋值操作的目标(a)是谁
RHS: 赋值操作右侧查询,对a的RHS可以理解为: 赋值操作中值(a)是多少
换句化说: 当变量出现在操作符左侧时进行LHS查询,为了查询到变量在哪.当变量出现在操作符右侧时进行RHS查询,为了查询变量的值是多少
a = 2是对a进行LHS,是为了找到a变量,并将2赋值给它
console.log(a)是对a进行RHS,是为了查找到a的值并传递给console.log
function foo(a){
var b = a;
return a+b
}
var c = foo(2)
上述的查询包含:
1.对c进行LHS c=foo(2)
2.对foo进行RHS c = foo(2)
3.对参数a进行LHS
4.对b进行LHS b=a
5.对a进行RHS b=a
6.对b和a分别进行RHS a+b
一共进行了三次LHS 四次RHS
为何区分RHS和LHS
因为在变量还未声明的时候,对变量的RHS和LHS的行为是不同的
RHS: RHS在所有嵌套作用域都无法查找该变量的时候,会直接抛出异常:ReferenceError引用错误
LHS: LHS在所有嵌套作用域都无法查找到该变量的时候.非严格模式下会在顶级作用域声明该变量(隐式声明),严格模式下会抛出异常: ReferenceError引用错误
顺带一提: 当对变量的值进行不合理操作的时候会抛出另一种异常: TypeError,如对非函数变量进行函数调用,访问null或undefined类型的值的属性时
作用域嵌套
当一个块或函数嵌套在另一个块和函数的时候,就发生了作用域嵌套,此时变量查找时,若没有在当前作用域找到,就会向外层嵌套的作用域进行查找直到找到该变量或查找到顶层作用域为止