JavaScript基础–作用域

JavaScript基础–作用域

什么是作用域

作用域定义了变量在哪查找怎样查找的一系列规则。通常我们在JS中定义的变量具有一定调用范围,比如全局变量可以在任意位置调用;而这个范围通常就是我们所指的作用域,JS中的作用域不同于C/C++,那么有哪些不同呢,请先和我一起了解一下JS简单的编译原理

编译原理

JS虽然是一门脚本语言,但是事实上它也是需要编译的(ps.通常脚本语言只需要“解释”,而不是“编译”)。但与其他传统的编译语言(例如java)不同的是,JS并不会提前编译好代码等待执行,而是很快被编译,然后再迅速执行(通常在几微秒以内)。即使有这样的差异,实际上它们的大致的编辑过程差不多,主要分为三个阶段: 
1. 分词/词法分析 
分词就是将表达式进行拆分,比如var a = 5;,编译引擎会将其分解为vara=5;。词法分析与分词差不多,它的作用是分析表达式中各个组成部分的意思,类似于一个句子中什么是主语、谓语等。 
2. 解析代码 
第二步,解析代码。在这个过程中会生成一个抽象语法树(AST,Abstract Syntax Tree),这个树上嵌套了第一步分解的各个“小元件”。还是以上面的表达式为例子,var a = 5;,这时根节点为var,它有两个子节点,一个是a,一个是==下面还有一个子节点5。 
AST树 
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就可以了。

注:调用witheval()改变执行环境作用域并不推荐使用,因为它们会带来性能问题,这里不详细解释,大家可以参考文章后面参考文献。

扩展:JS中的块级作用域

JS中的函数作用域带来了灵活性,但在一些地方有存在不方便,比如在forif语句中,我们可能只需要在它们的{}里定义一些临时的变量,并不希望污染当前的作用域。基于这些问题,ES6中新增了letconst这两个关键字来实现了块级作用域定义变量。 
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!
 

参考文献

posted @ 2016-01-06 14:37  卒兵  阅读(217)  评论(0编辑  收藏  举报