JavaScript基础–作用域
什么是作用域
作用域定义了变量在哪查找和怎样查找的一系列规则。通常我们在JS中定义的变量具有一定调用范围,比如全局变量可以在任意位置调用;而这个范围通常就是我们所指的作用域,JS中的作用域不同于C/C++,那么有哪些不同呢,请先和我一起了解一下JS简单的编译原理
编译原理
JS虽然是一门脚本语言,但是事实上它也是需要编译的(ps.通常脚本语言只需要“解释”,而不是“编译”)。但与其他传统的编译语言(例如java)不同的是,JS并不会提前编译好代码等待执行,而是很快被编译,然后再迅速执行(通常在几微秒以内)。即使有这样的差异,实际上它们的大致的编辑过程差不多,主要分为三个阶段:
1. 分词/词法分析
分词就是将表达式进行拆分,比如var a = 5;
,编译引擎会将其分解为var
,a
,=
,5
和;
。词法分析与分词差不多,它的作用是分析表达式中各个组成部分的意思,类似于一个句子中什么是主语、谓语等。
2. 解析代码
第二步,解析代码。在这个过程中会生成一个抽象语法树(AST,Abstract Syntax Tree),这个树上嵌套了第一步分解的各个“小元件”。还是以上面的表达式为例子,var a = 5;
,这时根节点为var
,它有两个子节点,一个是a
,一个是=
,=
下面还有一个子节点5
。
3. 代码生成
这个过程生成可以执行的代码,是面向机器的。这个过程中机器开始给变量分配内存,构建作用域等等。
理解作用域
JS代码在编译和执行的整个过程中,有三个“人物”扮演了重要的角色,分别是引擎、编译器和作用域。怎么理解这三个“人物”呢?引擎相当于这三者中的老大,负责JS代码的编译和执行;编译器负责解析代码、代码生成等;作用域负责管理代码中各个变量应该活动的范围。三人各司其职,保障JS在平台上的顺利运行。
工作流程
引擎、编译器和作用器各自承担成自己的责任,那么三者是如何协调工作的呢?以var a = 5;
为例。
1. 首先编译阶段,编译器会询问作用域那边是否有变量a
,如果有,则编译器忽略这句声明,如果没有,则在作用域中生成一个变量a
;
2. 在代码生成过程中,编译器就将5
赋给变量a
;
3. 最后在引擎在执行代码过程中,也会先问作用域哪里有没有a
,有就取它的值来用,如果没有,就去别的地方找(具体在哪里,继续往下看)。
查找方式
引擎在执行代码的过程中,比如var a = 5;
,很多书和博客将会搬出两个概念:左查找(LHS)和右查找(RHS),然后又解释了很多,其实左查找就是查找变量,右查找就是查找变量的值,如果这样理解的话,你就很容易知道什么时候用的左查找,什么时候用的右查找。
后来我自己研究了一下,发现左查找和右查找的唯一作用是标识引擎查找返回错误,如果找不到变量,就报ReferenceError
,找不到变量的值(属性),就报TypeError
。如果还有其他作用,欢迎补充。
嵌套作用域
嵌套作用域比较好理解,不过这里要注意两点:一个认识到JS是函数作用域,作用域以函数为单位来嵌套的;第二个是区分嵌套作用域与闭包的不同。
function foo(a) { console.log( a + b ); } var b = 2; foo( 2 ); // 4
上面的代码中就是一个简单的嵌套作用域的例子,全局定义了b
,在函数里定义了a
,被嵌套在全局作用域里的函数foo
可以调用b
,但全局作用域里不能调用a
。
引擎在查找变量时,是先从所执行的代码的作用域里开始查找的,如果找不到,再一级一级往外层的作用域里查找。
进一步理解作用域
在编译的阶段,作用域就形成,在引擎执行代码时,作用域的规则就开始限定引擎要怎么取变量或者值了。我们举个例子。
在上面的例子中,嵌套了三层作用域,第一层是全局作用域,第二层是函数foo
的作用域,第三层是函数bar
的作用域。引擎在执行某段代码时,查找的规则是这样的:
- 首先在当下的作用域里找,找的到就直接调用,找不到就想上一层作用域继续找,再找不到就再往上,直到全局作用域;
- 如果在当下的作用域下定义了一个与外层作用域相同的变量,以当下的作用域的为准。
在第一条规则中,定义变量时,需要在变量的前面加上var
,如果不加,在非严格的模式下,会在全局作用域创建这个变量,在严格模式下会报错。
在第二条规则中,如果在当下作用域中依然想调用全局那个重复的变量,可以这样定义window.a = 5;
,调用的时候直接去window.a
就可以了。
注:调用with
和eval()
改变执行环境作用域并不推荐使用,因为它们会带来性能问题,这里不详细解释,大家可以参考文章后面参考文献。
扩展:JS中的块级作用域
JS中的函数作用域带来了灵活性,但在一些地方有存在不方便,比如在for
、if
语句中,我们可能只需要在它们的{}
里定义一些临时的变量,并不希望污染当前的作用域。基于这些问题,ES6中新增了let
、const
这两个关键字来实现了块级作用域定义变量。 let
的用法和var
一样,只不过是作用域不同罢了例如:
for (let i =0; i<3; i++){ console.log(i); } // 0,1,2 cosole.log(i); //i is undefined
const
也是用来定义变量,不过它定义的是静态变量(常量),一旦定义了,重新赋值就会报错。
var foo = true; if (foo) { var a = 2; const b = 3; // 定义常量 a = 3; b = 4; // error! } console.log( a ); // 3 console.log( b ); // ReferenceError!
参考文献
- You-Dont-Know-JS
- JavaScript权威指南
- JavaScript高级程序设计