JavaScript温故而知新——执行环境和作用域

921 阅读5分钟

一、全局对象

我们都知道JavaScript中有一类非常重要的对象——全局对象(global object),它的属性是全局定义的符号,编写JavaScript代码时我们可以直接对这些属性进行使用。当JavaScript解释器启动时(浏览器加载新页面的时候),它将创建一个新的全局对象,并且定义一组初始的属性:

  • 全局属性,如undefinedInfinityNaN
  • 全局函数,如isNaN()parseInt()eval()
  • 构造函数,如Date()RegExp()String()Object()Array()
  • 全局对象,如MathJSON

在代码的最顶级——不在任何函数内的JavaScript代码中,可以使用this来引用全局对象:

var global = this;  // 定义一个引用全局对象的全局变量

在浏览器中,Window对象充当了全局对象,它的window属性引用其自身,可以代替this来引用全局对象。Window对象还针对Web浏览器和客户端JavaScript定义了额外的全局属性。另外,当我们在代码中声明了一个全局变量,这个全局变量就是全局对象的一个属性。

二、执行环境

执行环境可以说是JavaScript中最为重要的一个概念。

执行环境定义了变量或函数有权访问的其他数据,决定了它们各自的行为。

可以这么理解,每个执行环境都有一个与之关联的变量对象,我们在环境中定义的所有变量和函数都会保存在这个对象中,不过这个对象我们是无法访问的,它只供解析器在处理数据时在后台使用它。

全局执行环境——最外围的一个执行环境,在浏览器中,全局执行环境即全局对象window,因此所有全局变量和函数都可以作为window对象的属性和方法;
函数的执行环境——每一个函数都自己的执行环境,当代码执行到一个函数时,这个函数的环境就会被推入到一个环境栈中,函数执行完毕后,环境栈会将它的执行环境弹出,并将控制权返回给之前的执行环境。JavaScript代码的执行顺序便是由这一机制所控制着。
垃圾收集——当环境栈将一个执行环境弹出后,该环境便会被销毁,保存在其中的所有变量和函数定义也会随之销毁进而释放内存,这便是JavaScript的自动垃圾收集机制。而全局执行环境只有在程序退出,例如关闭网页时才会被销毁,因此对于全局变量或者全局对象的属性,一旦我们不再需要用到它们的时候,可以手动将其值设置为null来解除其引用,一旦引用被解除,它们便会脱离执行环境,以便垃圾收集器运行时将其回收,这样做能起到很好的优化内存占用的效果。

三、作用域

1.变量作用域

一个变量的作用域指在代码中定义这个变量的区域,全局变量拥有全局作用域,在任何地方都有定义。
而在函数内声明的变量只在函数体内有定义,它们是局部变量,作用域是局部性的。函数参数也是局部变量,只在函数体内有定义。
在函数体内,局部变量优先级高于全局变量,如果在函数内声明的变量或函数参数中带有的变量和全局变量重名,则全局变量会被局部变量所覆盖。
声明局部变量必须使用var语句

scope = "global";
function checkscope () {
    scope = "local";    // 修改了全局变量
    myscope = "local";  // 显示的声明了新的全局变量
    return [scope, myscope];
}
checkscope();   // ["local", "local"]
console.log(scope, myscope)     // "local","local"

2.函数作用域

在其他类似于C之类的语言中,花括号封闭的代码块都有自己的作用域,相当于JS中的执行环境。而JS中是没有块级作用域的,JS取而代之使用的是函数作用域,即变量在声明它们的函数体以及这个函数体内嵌套的任意函数体内都是有定义的。

function test(o) {
    if (o) {
        var i = o;                      // i在函数体内是有定义的,不仅是在if这个代码段内
        for(var j = 0; j < 10; j++) {   // j在函数体内是有定义的,不仅是在for循环内
            console.log(j);             // 输出0~9
        }
        console.log(j);                 // j已经定义了,输出10
    }
    console.log(i);                     // i已经定义了,但可能没有初始化
}

3.声明提前

我们知道了在JavaScript的函数作用域下,函数内声明的所有变量在函数体内是始终可见的,由于这一特性我们可能会出现一些误解。先看一段代码:

var scope = "global";
function f() {
    console.log(scope);     // 输出"undefined",而不是"global"
    var scope = "local";    // 变量在这里赋初始值,但变量本身在函数体内任何地方都是有定义的
    console.log(scope);     // 输出"local"
}

你可能会误以为函数中第一行输出global,但结果并非如此,这就是容易造成我们误解的地方。由于函数作用域的特性,局部变量在整个函数体始终是有定义的,这意味着变量在声明之前就可以使用了。
这个特性被称作:"声明提前"——即函数内的变量声明会被“提前”到函数体顶部,同时变量初始化留在原来的位置。
因此上面函数的代码实际上等价于:

function f() {
    var scope;              // 在函数顶部声明了局部变量
    console.log(scope);     // 变量存在,但其值是"undefined"
    scope = "local";        // 到这里才对变量进行初始化并赋值
    console.log(scope);
}

注意点

  • "声明提前"是在JavaScript引擎的"预编译"阶段进行的,即代码开始运行之前。
  • 由于JavaScript没有块级作用域,因此通常将变量声明放在函数体顶部,这使得我们的代码能更加清晰的反映出真实的变量作用域。

结尾

系列文章: