JavaScript有一套良好的规则用来存储变量,方便变量的查找,这套规则被称作作用域。
作用域的内部原理分为编译、执行、查询、嵌套和异常5个部分,下面对这5部分进行详细介绍。
编译
编译过程有3步:分词、解析和代码生成。下面以var a = 1;
为例进行这3个过程的说明。
分词(tokenizing)
把字符串分解成有意义的代码块,这些代码块被称为词法单元(token),词法单元组成词法单元流数组。
// var a = 1; 词法分析
[
"var" : "keyword",
"a" : "indentifier",
"=" : "assignment",
"1" : "integer",
";" : "eos" (end of statment)
]
解析(parsing)
把词法单元流数组转换成一个由元素逐级嵌套所组成的代表程序语法结构的树,这个树被称作“抽象语法树”(Abstract Syntax Tree, AST)。
{
operation: "=",
left: {
keyword: "var",
right: "a"
},
right: "1"
}
代码生成
把AST转换成可执行代码(机器指令)的过程称作代码生成。
JS引擎的编译过程其实很复杂,上面的三个是最基本的步骤,任何代码片段在执行前都要先进行编译,这个过程很短,通常是在代码执行前的几微妙完成。
执行
代码完成编译后接下来就是执行代码。同样以var a = 1;
为例,在编译完成后执行过程如下:
1、JS引擎会首先查询作用域,在当前的作用域集合中是否存在一个叫作a的变量。如果是,引擎就会使用这个变量;如果否,引擎会继续查找该变量。
2、如果引擎最终找到了变量a,就会将1赋值给它。否则引擎会抛出一个异常。
查询
JS引擎在执行过程中,对变量a进行了查询,这种查询成为LHS查询。查询分为两种:LHS(Left-hand Side)查询和RHS(Right-hand Side)查询。
可简单理解为变量出现在赋值操作符左边时进行LHS查询,它试图找到变量在内存中的引用地址。出现在右侧时进行RHS查询,它试图找到一个的具体的数据值。
function foo(a) {
console.log(a)
}
foo(1)
上面的代码查询过程如下:
- foo()对foo函数进行RHS引用
- 传参a=1,对a进行LHS引用
- console.log()对console对象进行RHS引用,查询是否有log方法
- console.log(a)对a进行RHS引用,把值传给console.log()
嵌套
如果在当前作用域中找不到某个变量,引擎会在外层嵌套的作用中继续查找,一直到最外层的全局作用域,直到找到该变量为止。
function foo(a) {
console.log(a+b)
}
var b = 2;
foo(1); // 3
上面的代码中,引擎会先在foo函数内部查找变量b,并尝试对其进行RHS引用。由于没找到,引擎会到上一层作用域即全局作用域中查找b,成功找到后对其进行RHS引用,把2赋值给b,最终返回3。
异常
当变量没有声明,尝试对变量进行LHS和RHS引用会出现不同的错误。
当RHS查询时,如果查询失败,引擎抛出ReferenceError(引用错误)异常
function foo(a){
a = b;
}
foo();//ReferenceError: b is not defined
当LHS查询时,如果无法找到变量,全局作用域会创建一个同名变量,并返回给引擎。严格模式下不会创建并返回同名变量,而是抛出ReferenceError异常。
// 示例1
function foo(){
a = 1;
}
foo();
console.log(a);//1
// 示例2
function foo(){
'use strict';
a = 1;
}
foo();
console.log(a);//ReferenceError: a is not defined