理解做用域

做用域是什么

一套用来储存变量的设计良好的规则,而且以后能够方便的找到这些变量。java

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

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

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

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

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

  • 用var a = 2;来作例子函数

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

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

总结:变量的赋值操做会执行两个动做,首先编译器会在当前做用域中声明一个变量(若是以前没有声明过),而后在运行时引擎会在做用域中查找该变量,若是可以找到就会对它赋值。token

关于LHS和RHS

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

总的来讲若是查找的目的是对变量进行赋值,那么就会使用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声明和函数声明混合在一块儿的时候,不然会出现问题

相关文章
相关标签/搜索