变量作用域
1. 函数作用域和声明提前
① javascript没有块级作用域,取而代之的使用了函数作用域,即变量在声明它们的函数体以及这个函数体嵌套的任意函数体内都是有定义的。
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);
}
console.log(k);
}
console.log(j);
}
② 声明提前:javascript函数里声明的所有变量(但不涉及赋值)都被“提前”至函数体的顶部,看一下如下代码:
var scope = "global";
function f() {
console.log(scope); // 输出"undefined",而不是"global"
var scope = "local"; // 变量在这里赋初始值,但变量本身在函数体内任何地方均是有定义的
console.log(scope); // 输出"local"
}
//以上代码等价于
function f() {
var scope:
console.log(scope);
scope = "local";
console.log(scope);
}
2. 作为属性的变量
当声明一个javascript全局变量时,实际上是定义了全局对象的一个属性。当使用var声明一个变量时,创建的这个属性是不可配置的,也就是说这个变量无法通过delete运算符删除。
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规范称为“声明上下文对象”。javascript可以允许使用this关键字来引用全局对象,却没有方法可以引用局部变量中存放的对象。这种存放局部变量的对象的特有性质,是一种对我们不可见的内部实现。即在局部变量的函数作用域之外是无法直接访问到该变量的,除非通过在函数作用域的对外方法获取到(类似类的私有属性跟公有方法)。
3. 作用域链
每一段javascript代码(全局代码或函数)都有一个与之关联的作用域链。这个作用域链是一个对象列表或者链表,这组对象定义了这段代码“作用域中”的变量。当javascript需要查找变量x的值的时候(这个过程称作“变量解析”),它会从链中的第一个对象开始查找,如果这个对象有一个名为x的属性,则会直接使用这个属性的值,如果第一个对象依然没有名为x的属性,则会继续查找下一个对象,以此类推。如果作用域链上没有任何一个对象含有属性x,那么就认为这段代码的作用域链上不存在x,并最终抛出一个引用错误异常。
在javascript的最顶层代码中,作用域链由一个全局对象组成,在这个作用域下声明的变量x都是全局对象(window)的一个属性,并且可以通过window.x或者this.x访问该变量。在不包含嵌套的函数体内,作用域链上有两个对象,第一个是定义函数参数和局部变量的对象(这个对象就是我们不可见的调用对象),第二个是全局对象。理解对象链的创建规则是非常重要的。当定义一个函数时,它实际上保存一个作用域链(此时作用域链中并没有值)。当调用这个函数时,它创建一个新的对象(调用对象)来存储它的局部变量,并将这个对象添加至保存的那个作用域链上,同时创建一个新的更长的表示函数调用作用域的“链”(其中包括全局对象)。
4. 对于全局对象与调用对象的理解(本人见解,不一定正确)
其实我们可以把全局对象想象成一个最顶级的调用对象,原本调用对象的内部实现是不可见的,即我们不可以直接访问到调用对象。但全局对象是个例外,它唯一与调用对象的区别无外乎ECMAScript对其强制规定可见(即一个对外可见的调用对象)。全局代码中的执行环境相当于一个最顶级的函数体内部,页面加载时,浏览器默认执行这个顶级函数而已。