理解作用域

原创2016-11-15 13:41:43242
摘要:要理解作用域,先了解一下编译原理分词/词法分析(Tokenizing/Lexing) 这个过程会将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块被称为词法单元(token)。例如,考虑程序var a = 2;。这段程序通常会被分解成为下面这些词法单元:var、a、=、2 、; 空格是否会被当作词法单元,取决于空格在这门语言中是否具有意义。解析/语法分析(Parsin

要理解作用域,先了解一下编译原理

分词/词法分析(Tokenizing/Lexing) 
这个过程会将由字符组成的字符串分解成(对编程语言来说)有意义的代码块,这些代码块被称为词法单元(token)。例如,考虑程序var a = 2;。这段程序通常会被分解成为下面这些词法单元:var、a、=、2 、; 空格是否会被当作词法单元,取决于空格在这门语言中是否具有意义。

解析/语法分析(Parsing) 
这个过程是将词法单元流(数组)转换成一个由元素逐级嵌套所组成的代表了结构的树。这个树被称为“抽象语法树”(Abstract Syntax Tree,AST)。

代码生成
将AST转换为可执行代码的过程称被称为代码生成。这个过程与语言、目标平台等息息相关。抛开具体细节,简单来说就是有某种方法可以将var a = 2;的AST转化为一组机器指令,用来创建一个叫作a的变量(包括分配内存等),并将一个值储存在a中。

理解作用域,编译器,引擎的关系

用var a = 2;来做例子

首先,编译器遇到var a,编译器会询问同一个作用域的集合中是否存在该变量。如果是,编译器会忽略该声明, 继续编译。否则它会要求作用域在当前作用域的集合中声明一个新的变量,并命名为a。

接下来,编译器会为引擎生成运行时所需的代码,这些代码被用来处理a = 2这个赋值操作。引擎运行时会首先询问作用域,在当前的作用域集合中是否存在一个叫作a的变量。如果是,引擎就会使用这个变量;如果否,引擎会继续查找该变量(从作用域链中)。如果引擎最终找到了a变量,就会将2赋值给它。否则引擎就会举手示意并抛出一个异常!

总结:变量的赋值操作会执行两个动作,首先编译器会在当前作用域中声明一个变量(如果之前没有声明过),然后在运行时引擎会在作用域中查找该变量,如果能够找到就会对它赋值。

关于LHS和RHS

在引擎查找变量a来判断它是否声明过,,但是引擎执行怎样的查找,会影响最终的查找结果。在例子中,引擎会为变量a进行LHS查询。另外一个查找的类型叫作RHS。
我打赌你一定能猜到“L”和“R”的含义,它们分别代表左侧和右侧。
什么东西的左侧和右侧?是一个赋值操作的左侧和右侧。
换句话说,当变量出现在赋值操作的左侧时进行LHS查询,出现在右侧时进行RHS查询。

总的来说如果查找的目的是对变量进行赋值,那么就会使用LHS查询;如果目的是获取变量的值,就会使用RHS查询。

作用域嵌套

作用域嵌套当一个块或函数嵌套在另一个块或函数中时,就发生了作用域的嵌套。因此,在当前作用域中无法找到某个变量时,引擎就会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层的作用(也就是全局作用域)为止。考虑以下代码:

function foo(a) {
console.log( a + b );
}
var b = 2;
foo( 2 ); // 4

作用域共有两种主要的工作模型。第一种是最为普遍的,被大多数编程语言所采用的词法作用域。另外一种叫作动态作用域,仍有一些编程语言在使用(比如Bash脚本、Perl中的一些模式等)。

词法作用域

词法作用域就是定义在词法阶段的作用域,换句话说,词法作用域是由你写在代码时将变量和代码块作用域写在哪里来决定。看下面代码

function foo(a){
    var b = a*2;
    fucntion bar(c){
        console.log(a,b,c);
    }
    bar(b*3);
}
foo(2);//2,4,12
//这个例子中有三个助剂嵌套的作用域,其中有一个标识符:foo
//包含着foo所创建的作用域,其中有三个标识符:a, bar 和 b
//包含bar所创建的作用域,其中只有一个标识符:c

作用域查找会从运行时所在时所处的最内部作用域开始,逐级向上直到找到第一个匹配的标识符时停止。

无论函数在哪里被调用,也无论如何被调用,它的词法作用域都只由函数被声明时所处的位置决定。
看下面代码

var name = '小红';
function showName() {
    console.info(name);
}

function show() {
    var name = '小黑';
    showName();
} 
show();//小红

如果你记住并且理解了上面的话,那么应该可以得到这个结果。用作用域链的角度解析:执行show()函数时,进入function show(){}的作用域内,然后执行showName()函数,再进入到function showName(){}的作用域内,要输出name,就在当前作用域找,但是找不到,然后就向上爬一层,在全局环境中找到了var name = '小红';,所以show()就输出了小红

函数中的作用域

函数作用域的含义是指,属于这个函数的全部变量都可以在整个函数的范围内使用及复用(事实上在嵌套的作用域中也可以使用)

提升

无论作用域中的声明出现在什么地方,都将在代码本身被执行前首先进行处理。可以将这个过程形象地想象成所有的声明(变量和函数)都会被“移动”到各自作用域的最顶端,这个过程被称为 提升 。
任何声明在某个作用域内的变量,都将附属于这个作用域。但是作用域同其中的变量声明出现的位置有某种微妙的关系。

考虑一下代码

a = 2
var a;
console.log(a);//2

说明先有声明后有赋值
只有声明本身才会被提升,而赋值或其他运行逻辑会留在原地。如果提升改变代码的执行顺序,会造成非常严重的破坏。
再看个例子

foo();
function(){
    console.log(a);//undefined
    var a = 2;
}
 //foo函数的声明被提升了,因此函数第一行中的调用可以正常执行,函数内部对var a进行提升。因此上面的代码会被理解成下面的形式。

function foo(){
    var a;
    console.log(a);
    a = 2;
}
foo();

函数优先

函数声明和变量声明都会被提升。但首先是被提升,然后才是变量。考虑如下代码

foo()
var foo;
function foo(){
    console.log(1);
}
foo = function(){
    console.log(2);
};
//会输出1而不是2,这个代码片段会被解释成

function foo(){
    console.log(1);
}
foo();//1
foo = function(){
    console.log(2);
}

//出现在后面的函数声明还是可以覆盖前面的
foo();//3
function foo(){
    console.log(1);
}
var foo = function(){
    console.log(2);
}
function foo(){
    console.log(3);
}

最后要注意避免重复声明,特别是当普通的var声明和函数声明混合在一起的时候,否则会出现问题

发布手记

热门词条