什么是作用域?
作用域相当于一套设计良好的用于存储变量并易于访问这些变量的规则。
作用域根据确定的周期不同分为静态作用域和动态作用域。
JavaScript中不存在动态作用域,故在此不进行讨论。
静态作用域又叫做词法作用域,采用词法作用域的变量叫做词法变量,词法变量会在编译时确定它的作用域。
变量赋值
为了理解作用域,我们来分析一下JavaScript中的变量赋值操作。
var a = 2;
当看到这条赋值语句时,我们很可能把它当成是一句声明,但其实这句话执行了两个不同的动作:首先编译器会询问该作用域当前是否有一个该名称的变量存在,如果有,则忽略该声明,否则声明变量a;然后在运行时引擎会在该作用域中查找该变量,并对变量a赋值2。
区分LHS和RHS赋值
当变量出现在赋值操作的左侧时进行LHS查询,出现在右侧时进行RHS查询。详细一点来讲,RHS只是为了找到当前变量存储的值,而LHS是为了找到当前变量的位置并把相应的值赋给它。
所以RHS查询并不一定发生在等号的右侧,如下
console.log(a); // 对a执行RHS查询
a = 2; // 对a执行LHS操作
而在调用一个函数时,赋值操作的情况就更为复杂了
function foo(a) {
console.log(a);
}
foo(1);.
这个例子首先对foo这个函数名进行了RHS查询,然后在括号里进行了一次隐性的LHS调用,也就是a = 1的赋值动作,最后在console.log(a)对a进行了一次RHS查询。
为什么区分LHS和RHS是一件重要的事?
因为在变量还没有声明的情况下,两种查询的行为是不一样的。
function foo() {
console.log(a + b)
}
var a = 5;
foo();
由于在上述函数中对b执行RHS查询时无法找到该变量,也就是变量b未被声明,引擎会抛出ReferenceError异常
而在进行LHS查询时,如上例中的var a = 5;如果没有找到该变量,全局作用域中会创建一个该名称的变量(非严格模式下),ES5引入了严格模式,同正常模式相比,严格模式下禁止自动或隐式地创建全局变量,所以在严格模式下LHS查询失败时不会在全局作用域创建并返回一个全局变量,而是返回一个ReferenceError异常。
另外,在进行RHS查询时,如果该变量名存在,但执行操作不符合变量类型,比如对一个非函数类型的值进行函数调用,或者引用null或undefined类型的值中的属性,引擎会抛出另一种类型的异常,叫做TypeError。
作用域的嵌套
当一个块或者函数嵌套在另一个块或者函数中时就发生了作用域的嵌套。JavaScript中对作用域嵌套的处理比较特殊,在当前作用域中无法找到某个变量时,引擎会在外层嵌套的作用域中继续查找,直到找到该变量,或抵达最外层作用域(全局作用域)为止。
function foo(a) {
console.log(a + b);
}
var b = 2;
foo(1); // 输出为3
遮蔽效应
作用域查找会在找到第一个匹配的变量标识符时停止。在多层的嵌套作用域中,如果存在同名标识符,则内部的标识符会“遮蔽”外部的标识符。
全局作用域定义的变量会自动成为全局对象的属性,所以可以通过全局对象属性的引用来对其进行访问,但非全局的变量如果被遮蔽了,则无论如何也无法被访问。