JavaScript温故而知新——作用域链和闭包

930 阅读4分钟

一、作用域链

JavaScript是基于词法作用域的语言,当代码在一个环境中执行时,就会创建与之关联的作用域链(scope chain),这个作用域链可以看做是一个对象或者链表,对象中定义了这段代码“作用域”中的所有变量。
当需要查找某一变量的值时(这个过程称作“变量解析”),JavaScript会从链中的第一个对象开始查找,如果找到了则直接使用这个属性的值,查找不到则继续查找链上的下一个对象,以此类推,如果整个作用域链中都没有任一对象包含该属性,则会抛出一个引用错误(ReferenceError)异常。

  • 如果执行环境是函数,则其活动对象最开始只包含一个变量,即arguments对象(这个对象在全局环境是不存在的)
  • 全局执行环境的变量对象始终都是作用域链中的最后一个对象。

可以通过一段代码来理解作用域链:

var color = "blue";
function changeColor() {
    var anotherColor = "red";
    function swapColors() {
        var tempColor = antherColor;
        antherColor = color;
        color = tempColor;
        // 这里可以访问color、anotherColor和tempColor
    }
    // 这里可以访问color和anotherColor,但不能访问tempColor
    swapColors();
}
// 这里只能访问color
changeColor();

作用域链图:

图中矩形表示了三个特定的执行环境:全局环境、changeColor()的局部环境和swapColors()的局部环境。其中,内部环境可以通过作用域链访问所有的外部环境,但外部环境不能访问内部环境中的任何变量或函数。

二、闭包

理解了作用域链,我们再来理解闭包是什么。
我们都知道在JavaScript中,只有函数内部的子函数能够访问局部变量,那么如果函数外部要访问函数内的局部变量应该怎么做呢?闭包就是用来解决这一问题的。

闭包是指有权访问另一个函数作用域中的变量的函数。

可以看一下阮一峰老师的例子:

function f1() {
    var n = 999;
    nAdd = function() { n += 1 }
    function f2() {
        alert(n);
    }
    return f2;
}

var result = f1();
result(); // 999
nAdd();
result(); // 1000   

在这个例子中,result就是闭包f2函数,第一次运行的值是999,说明访问到了函数f1内部的变量。第二次运行的值是1000,这是由于f2被赋给了全局变量result,由于result引用着f2,因此f2不会因为运行结束了就被销毁,它将一直存在于内存当中,f1和它的局部变量n也是如此。

f1nAdd由于没有使用var进行声明,因此aAdd是一个全局变量,f1执行后将function() { n + 1 }赋给了全局变量nAdd,此时function() { n + 1 }也是一个闭包,同样可以对f1内部的变量进行操作。

总结

  • 闭包可以读取函数内部的变量;
  • 闭包会使得函数内部的变量都被保存在内存中,造成较大的内存开销,因此不要滥用闭包。解决的方法是在退出函数之前将不使用的局部变量置为null

关于this对象

了解完闭包,还有一个需要特别注意的点。就是在闭包中使用this的问题,看下面的代码:

var name = "The Window";
var object = {
    name: "My Object",
    getName: function() {
        return function() {
            return this.name;
        };
    }
};
alert(object.getNameFunc()());  // "The Window"(在非严格模式下)

这里要注意两个点:

  • this对象是在运行时基于函数的执行环境绑定的
  • 匿名函数的执行环境具有全局性

因此这里的this指向的是全局环境,所以查找到的是全局的name

那么闭包如何访问到object对象呢,我们可以改变一下代码:

var name = "The Window";
var object = {
    name: "My Object",
    getName: function() {
        var that = this;
        return function() {
            return that.name;
        };
    }
};
alert(object.getNameFunc()());  // "My Object" 

可以看到,将this对象保存在一个闭包能够访问到的变量哩,就可以让闭包访问到该对象了。

结尾

系列文章: