闭包是一个函数,可以访问它被创建时所处的上下文环境。

我对它一开始的理解是,保存了一份上下文的拷贝,然而这个理解是错的。实际是所需的上下文没有被释放。

闭包的例子

如果用闭包做一个计数器,

function newCounter(start){
    // context
    var n = start || 0; // if start is undefined
    return {
        // Plan A: here will be anthor 'n' to store the 'n' from the context
        inc: function(){
            return ++n;  
        }
    }
}
// usage
var counter = newCounter();
counter.inc(); // 1
counter.inc(); // 2

对于这个含有函数(inc())的匿名对象,它是如何访问到n,有两个解释:

错误的理解:闭包会复制上下文

闭包是自带状态的函数,看起来它的实现是这样的,

function closure{
    var arguments = [...];
    function real_funcion(arguments){
        /* code here*/   
    }
}

如此来保存上下文。因为函数本身就是一个 Object,所以其实闭包看起来就是一个带有一个可执行函数和数据的 Object。

看起来,闭包将创造这个对象外面的上下文保存在内部,对外不可见。在计数器的例子中,返回的 Object 中的函数创建一个新的 n 对外隐藏,拷贝前面执行完毕的 n,形成一个闭包。

但是,这个现象很奇怪:

function returner(){
    var i = 0;
    var closure = function(){
        return i;
    }
    i++;
    return closure;
}
// usage
var c = returner();
c(); // 1

另一个例子,

function evens(){
    var arr = [];
    for (var i = 0; i<3; i++){
        arr.push(function(){
            return i * 2;
            });
    }
    return arr;
}
// usage
var e = evens();
// lazy method, calculate when use
e[0](); // 6
e[1](); // 6
e[2](); // 6

这个问题的一个解释是:

看起来,闭包复制上下文并不在声明它的时候,而在上下文函数结束后。

而正确的解释是,闭包需要的外部变量会持续保留

正确的理解:闭包需要的外部变量会持续保留

要验证它,我们给 counter 添加一个 now() 函数(也是个闭包),

function newCounter(start){
    // context
    var n = start || 0; // if start is undefined
    return {
        // here will be anthor 'n' to store the 'n' from the context
        inc: function(){
            return ++n;  
        },
        now: function(){
            return n;
        }
    }
}
// usage
var counter = newCounter();
counter.inc(); // 1
counter.inc(); // 2
counter.now(); // 2

可以看到,now() 使用的n是和inc()所共用的,这两个函数并没有单独各自复制一个n

evens()

要修复 evens() 函数的问题,

function evens(){
    var arr = [];
    for (var i = 0; i<3; i++){
        arr.push((function(n){
            return function(){
                return n * 2;
            }
        })(i));
    }
    return arr;
}
// usage
var e = evens();
e[0](); // 0
e[1](); // 2
e[2](); // 4

我们为 ()=> return n * 2 这个函数包裹上一个上下文,即 (function(n){...})(n) 这是一个立即执行的匿名函数,唯一的目的是创建一个新的函数作用域。

上下文和作用域

函数A的上下文的定义:

定义函数A的外部函数的参数和变量,除了 this, arguments

上面定义的 counter 所引用的 n 访问的就是创建这个闭包的那个父函数中的 n

创建新的作用域为什么有效?

因为不同作用域内的变量生命周期不同。

作用域举例,

var foo = function(){
    var a = 3, b = 5;
    var bar = function(){
        var b = 7, c = 11;
        // 定义相同的 b,引用时会首先访问 this.b,找不到去找是 super.b
        // 此时此地,a = 3, b = 7, c = 11
        a += b + c;
        // 此时此地,a = 21, b = 7, c = 11
    }
    // 此时此地,a = 3, b = 5, c = undefined
    bar();
    // 此时此地,a = 21, b = 5, c = undefined
}

可以看到,内部定义的b的生命周期和外部的不同。这个例子中,bar()内的变量的生存周期比外部的短,在执行完毕后就释放了。

对于闭包来说,这个函数的生存周期比包含它的外部函数的生存周期更长。而本应释放的外部变量,因为内部函数仍然存在引用,故予以保留,不予释放。

evens() 这个例子中,我们让内部函数访问的 n 是在每次循环中新产生的 n 。因此得到的三个闭包所处的上下文是不同的。

总结

看起来,闭包中的 n 就如同新建一个 private 变量。

但是其实只是上下文的变量没有释放而已。