入门 JavaScript 闭包(Closure)

闭包(closure)是JavaScript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。

变量的作用域

要理解闭包,首先必须理解 JavaScript 特殊的变量作用域。

变量的作用域无非就是两种:全局和局部。

1
2
3
4
5
6
7
let n = 114514;

function foo(){
    alert(n);
}

foo(); // 114514

另一方面,在函数外部自然无法读取函数内的局部变量。

1
2
3
4
5
6
7
function foo() {
    let n = 114514;
}

foo();

alert(n); // ReferenceError: n is not defined

这里需要注意,函数内部声明变量的时候,一定要使用 letimmutable 不可变对象可以使用 const),而不是使用 var 或者不写。如果不使用 let 的话,你实际上声明了一个全局变量!

letconst 是 es6 标准引入的,这里就不建议使用老标准的 var 了,因为涉及到作用域提升的话题会耗费很多篇幅,初学者记住用 let 就对了。

1
2
3
4
5
6
7
function foo() {
    n = 114514;
}

foo();

alert(n); // 114514

什么是闭包

有时我们有时候需要使用函数内的局部变量,但是从函数作用域外部获取函数作用域内的变量是不可能的。

那么在函数的内部,再定义一个函数怎么样呢。

1
2
3
4
5
6
7
function foo() {
    let n = 114514;

    function bar() {
        alert(n); // 114514
    }
}

在上面的代码中,函数 bar 就被包括在函数 foo 内部,这时 foo 内部的所有局部变量,对 bar 都是可见的。但是反过来就不行, bar 内部的局部变量,对 foo 就是不可见的。这就是 JavaScript 语言特有的“作用域链(Scope Chain)”结构,子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。

不仅如此,既然 bar 可以读取 foo 中的局部变量,那么只要把 bar 函数作为返回值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
function foo() {
    let n = 114514;

    function bar() {
        alert(n); 
    }

    return bar;
}

let result = foo();

result(); // 114514

其实这里的 bar 函数,就是一个闭包函数。

各种专业文献上的"闭包"(closure)定义非常抽象。其实简单来说,闭包就是函数中的函数,能够读取其他函数内部变量的函数。

闭包的用途

闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。

怎么来理解这句话呢?请看下面的代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function foo() {
    let n = 114514;

    function increment() {
        n += 1;
    }

    function popup(){
        alert(n);
    }

    return [increment, popup];
}

let [hoge, fuga] = foo();

// hoge = foo->increment, fuga = foo->popup

fuga(); // popup: 114514

hoge(); // increment: n => 114515

fuga(); // popup: 114515

在这段代码中,fuga 实际上就是闭包 popup 函数。它一共运行了两次,第一次的值是114514,第二次的值是114515。这证明了,函数 foo 中的局部变量 n 一直保存在内存中,并没有在 foo 调用后被自动清除。

为什么会这样呢?原因就在于 foopopup 的父函数,而 popup 被赋给了一个全局变量,这导致 popup 始终在内存中,而 popup 的存在依赖于 foo ,因此 foo 也始终在内存中,不会在调用结束后,被垃圾回收机制(Garbage collection)回收。

使用闭包的注意点

  1. 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题。

  2. 闭包可以改变父函数局部变量的值。所以,如果你把父函数当作对象(Object)使用,把闭包当作它的公用方法(Public method),把内部变量当作它的私有属性(Private value),这时一定要小心,不要随便改变父函数内部变量的值。

思考题

如果你能理解下面两段代码的运行结果,应该就算理解闭包的运行机制了。

代码片段一。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
let name = 'Shimokitazawa';
let object = {
    name: 'Yaju',
    getName: function() {
        return function() {
            return this.name;
        };
    },
};

alert(object.getName()());

代码片段二。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
let name = 'Shimokitazawa';
let object = {
    name: 'Yaju',
    getName: function() {
        let that = this;
        return function() {
            return that.name;
        };
    },
};

alert(object.getName()());

(参考:http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html,本文对其中有问题的点进行了修改。)