3.10 变量作用域
一个变量的作用域(scope)是程序源代码中定义这个变量的区域。全局变量拥有全局作用域,在JavaScript 代码中的任何地方都是有定义的。然而在函数内声明的变量只在函数体内有定义。它们是局部变量,作用域是局部性的。函数参数也是局部变量,它们只在函数体内有定义。
在函数体内,局部变量的优先级高于同名的全局变量。如果在函数内声明的一个局部变量或者函数参数中带有的变量和全局变量重名,那么全局变量就被局部变量所遮盖。
var scope = "global"; // 声明一个全局变量
function checkscope() {
var scope = "local"; // 声明一个同名的局部变量
return scope; //返回局部变量的值,而不是全局变量的值
}
checkscope() // => "local"
var hzh = "黄子涵是帅哥!";
function huangzihan() {
var hzh = "黄子涵是靓仔!"
return hzh;
}
console.log(huangzihan());
[Running] node "e:\HMV\JavaScript\JavaScript.js"
黄子涵是靓仔!
[Done] exited with code=0 in 0.207 seconds
尽管在全局作用域编写代码时可以不写 var 语句,但声明局部变量时则必须使用 var 语句。思考一下如果不这样做会怎样:
scope = "global"; // 声明一个全局变量,甚至不用var来声明
function checkscope2() {
scope = "local"; // 糟糕!我们刚修改了全局变量
myscope = "local"; // 这里显式地声明了一个新的全局变量
return [scope, myscope]; // 返回两个值
}
checkscope2() // => ["local", "local"]: 产生了副作用
scope // =〉"local": 全局变量修改了
myscope // => "local": 全局命名空间搞乱了
hzh = "黄子涵是帅哥!";
function huangzihan() {
hzh = "黄子涵是靓仔!"
hcq = "黄子涵是靓仔!";
return [hzh, hcq];
}
console.log(huangzihan());
console.log("hzh = " + hzh);
console.log("hcq = " + hcq);
[Running] node "e:\HMV\JavaScript\JavaScript.js"
[ '黄子涵是靓仔!', '黄子涵是靓仔!' ]
hzh = 黄子涵是靓仔!
hcq = 黄子涵是靓仔!
[Done] exited with code=0 in 0.181 seconds
函数定义是可以嵌套的。由于每个函数都有它自己的作用域,因此会出现几个局部作用域嵌套的情况,例如:
var scope = "global scope"; // 全局变量
function checkscope() {
var scope = "local scope"; // 局部变量
function nested() {
var scope = "nested scope"; // 嵌套作用域内的局部变量
return scope; // 返回当前作用域内的值
}
return nested();
}
checkscope() // => "嵌套作用域"
var hzh = "全局的黄子涵";
function hzhGlobal() {
var hzh = "局部的黄子涵";
function hzhLocal() {
var hzh = "嵌套的局部的黄子涵";
return hzh;
}
return hzhLocal();
}
console.log(hzhGlobal());
[Running] node "e:\HMV\JavaScript\JavaScript.js"
嵌套的局部的黄子涵
[Done] exited with code=0 in 0.2 seconds
3.10.1 函数作用域和声明提前
在一些类似c语言的编程语言中,花括号内的每一段代码都具有各自的作用域,而且变量在声明它们的代码段之外是不可见的,我们称为块级作用域(block scope),而JavaScript中没有块级作用域。JavaScript取而代之地使用了函数作用域(function scope):变量在声明它们的函数体以及这个函数体嵌套的任意函数体内都是有定义的。
在如下所示的代码中,在不同位置定义了变量i、j和k,它们都在同一个作用域内一这三个变量在函数体内均是有定义的。
function test(o) {
var i = 0; // i在整个函数体内均是有定义的
if (typeof o == "object") {
var j = 0; // j在函数体内是有定义的,不仅仅是在这个代码段内
for(var k=0; k < 10; k++) { // k在函数体内是有定义的,不仅仅是在循环体内
console.log(k); // 输出数字0 ~ 9
}
console.log(k); // k已经定义了,输出10
}
console.log(j); // j已经定义了,但可能没有初始化
}
JavaScript 的函数作用域是指在函数内声明的所有变量在函数体内始终是可见的。有意思的是,这意味着变量在声明之前甚至已经可用。JavaScript 的这个特性被非正式地称为声明提前(hoisting),即JavaScript 函数里声明的所有变量(但不涉及赋值)都被“提前”至函数体的顶部,看一下如下代码:
var scope = "global";
function f() {
console.log(scope); // 输出"undefined",而不是"global"
var scope = "local”; // 变量在这里赋初始值,但变量本身在函数体内任何地方均是有定义的
console.log(scope); // 输出"local"
}
注
“声明提前”这步操作是在JavaScript引擎的“预编译”时进行的,是在代码开始运行之前。
你可能会误以为函数中的第一行会输出“global”,因为代码还没有执行到var语句声明局部变量的地方。其实不然,由于函数作用域的特性,局部变量在整个函数体始终是有定义的,也就是说,在函数体内局部变量遮盖了同名全局变量。尽管如此,只有在程序执行到var语句的时候,局部变量才会被真正赋值。因此,上述过程等价于:将函数内的变量声明“提前”至函数体顶部,同时变量初始化留在原来的位置:
function f() {
var scope; // 在函数顶部声明了局部变量
console.log(scope); // 变量存在,但其值是"undefined"
scope = "local"; // 这里将其初始化并赋值
console.log(scope); // 这里它具有了我们所期望的值
}
在具有块级作用域的编程语言中,在狭小的作用域里让变量声明和使用变量的代码尽可能靠近彼此,通常来讲,这是一个非常不错的编程习惯。由于JavaScript没有块级作用域,因此一些程序员特意将变量声明放在函数体顶部,而不是将声明靠近放在使用变量之处。这种做法使得他们的源代码非常清晰地反映了真实的变量作用域。
3.10.2 作为属性的变量
当声明一个 JavaScript 全局变量时,实际上是定义了全局对象的一个属性。当使用var声明一个变量时,创建的这个属性是不可配置的,也就是说这个变量无法通过 delete 运算符删除。可能你已经注意到了,如果你没有使用严格模式并给 一个未声明的变量赋值的话,JavaScript 会自动创建一个全局变量。以这种方式创建的变量是全局对象的正常的可配值属性,并可以删除它们:
var truevar = 1; // 声明一个不可删的全局变量
fakevar = 2; // 创建全局对象的一个可删除的属性
this.fakevar2 = 3; // 同上
delete truevar // => false: 变量并没有被删除
delete fakevar // => true: 变量被删除
delete this.fakevar2 // => true: 变量被删除
JavaScript 全局变量是全局对象的属性,这是在 ECMAScript 规范中强制规定的。对于局部变量则没有如此规定,但我们可以想象得到,局部变量当做跟函数调用相关的某个对象的属性。ECMAScript 3规范称该对象为“调用对象” (call object), ECMAScript 5规 范称为“声明上下文对象”(declarative environment record)。JavaScript 可以允许使用this关键字来引用全局对象,却没有方法可以引用局部变量中存放的对象。这种存放局部变量的对象的特有性质,是一种对我们不可见的内部实现。然而,这些局部变量对象存在的观念是非常重要的。
3.10.3 作用域链
JavaScript 是基于词法作用域的语言:通过阅读包含变量定义在内的数行源码就能知道变量的作用域。全局变量在程序中始终都是有定义的。局部变量在声明它的函数体内以及其所嵌套的函数内始终是有定义的。
如果将一个局部变量看做是自定义实现的对象的属性的话,那么可以换个角度来解读变量作用域。每一段JavaScript 代码(全局代码或函数)都有一个与之关联的作用域链 (scope chain) 。这个作用域链是一个对象列表或者链表,这组对象定义了这段代码 “作用域中”的变量。当JavaScript 需要査找变量x的值的时候(这个过程称做“变量解析”(variable resolution)),它会从链中的第一个对象开始査找,如果这个对象有一个名为 x 的属性,则会直接使用这个属性的值,如果第一个对象中不存在名为 x 的属性, JavaScript 会继续査找链上的下一个对象。如果第二个对象依然没有名为 x 的属性,则会继续査找下一个对象,以此类推。如果作用域链上没有任何一个对象含有属性X,那么就认为这段代码的作用域链上不存在X,并最终抛出一个引用错误(ReferenceError)异常。
在 JavaScript 的最顶层代码中(也就是不包含在任何函数定义内的代码),作用域链由一个全局对象组成。在不包含嵌套的函数体内,作用域链上有两个对象,第一个是定义函数参数和局部变量的对象,第二个是全局对象。在一个嵌套的函数体内,作用域链上至少有三个对象。理解对象链的创建规则是非常重要的。当定义一个函数时,它实际上保存一个作用域链。当调用这个函数时,它创建一个新的对象来存储它的局部变量,并将这个对象添加至保存的那个作用域链上,同时创建一个新的更长的表示函数调用作用域的“链”。对于嵌套函数来讲,事情变得更加有趣,每次调用外部函数时,内部函数又会重新定义一遍。因为每次调用外部函数的时候,作用域链都是不同的。内部函数在每次定义的时候都有微妙的差别一在每次调用外部函数时,内部函数的代码都是相同的,而且关联这段代码的作用域链也不相同。
作用域链的概念对于理解 with 语句是非常有帮助的,同样对理解闭包的概念也至关重要。