共计 2096 个字符,预计需要花费 6 分钟才能阅读完成。
什么是闭包?“闭包是指有权访问另一个函数作用域中的变量的函数。”—《JavaScript 高级程序设计》通常来说,当一个函数可以访问另一个函数内部定义的变量 (包括属性和方法) 时,这个函数可以称之为闭包:
function fnA(){
var a = “this is fnA.a”;
return function fnB(){
alert(a);
}
}
var x = fnA();
x(); // “this is fnA.a”
例子中,我们可以通过 x(即 fnB)去访问 fnA 中的内部变量(a),此时我们可以称 fnB 为闭包。
闭包是如何产生的?为了更清楚的解释闭包的发生,我们需要先明白“函数的创建”到“函数的调用”到底发生了什么事情。
1、函数被创建时,会创建一条作用域链 (下称 A 链)。然后根据跟创建时的环境,依照“外部函数”、“‘外部函数’的外部函数”、“‘外部函数的外部函数’的外部函数”….“全局函数”顺序,将所有函数的活动对象(可以简单理解为所有的内部变量) 添加到这条作用域链上。(大多数非闭包的情况下,函数的外部函数即全局变量)2、函数被调用时,也会创建一条作用域链 (下称 B 链),并将 A 链的内容包含到 B 链中,然后将当前函数的活动对象(可以简单理解为所有的内部变量) 添加到 B 链条的顶端。3、当访问函数内部变量时,会按照 B 链中的变量保存的顺序依次访问。即内部变量,(创建时的)外部函数的变量,(创建时的)外部函数的外部函数的变量 … 全局变量。
下面是一道经典的闭包题:
function fun(n,o) {
console.log(o)
return {
fun:function(m){
return fun(m,n);
}
};
}
var a = fun(0); // undefined。由于会“o”未赋值,所以会显示:undefined。同时返回一个字面量对象,对象内创建一个名为“fun”的函数,并将对象返回赋值给全局变量 a。此时 a 内部的函数 fun 已经被创建好了,它的作用域链上包含了外部函数 (外层的 fun 函数) 的所有变量,其中包含了 n(值为 0),o(值为 undefined);以及全局函数的变量 fun(值得注意的是,这个 fun 属于全局函数的变量)。
a.fun(1); // 0。上面提到。在创建 a 的内部 fun 时,它包含的作用域链中包含了 n(值为 0),o(值为 undefined);以及全局函数的变量 fun。因此,我们调用 (访问) 的“fun”是作用域链中给全局函数的函数 fun。m=1,n=0,将其赋值给全局函数的函数 fun,即:n=(m=)1,o=(n=)0,打印 0,值为“0”。
a.fun(2); // 0
a.fun(3); // 0。这里有个“坑”需要注意。在上个步骤“a.fun(1);”中,最后会创建一个对象 (fun 函数作用域链中的 n 值为 1,o 值为 0) 并返回。但是并没有变量来接收这个对象,更不会影响到 a 内部作用域链。因此“a.fun(2);”、“a.fun(3);”中,作用域链上的值与“a.fun(1);”中完全一样。
var b = fun(0).fun(1).fun(2).fun(3); // undefined,0,1,2
// 这是一条链式调用。为了便于理解,我们将链式调用拆分以下等价的方案:
var b1 = fun(0); // undefined。这个和“var a = fun(0);”,不重复解释。
var b2 = b1.fun(1); // 0。这里和“a.fun(1);”一样,不重复解释。但是要注意的是,此时有个变量 b2 接收了 b1.fun 返回的变量。此时,b2 中的函数 fun 的作用域链的 (部分) 内容情况:n=1,o=0。
var b3 = b2.fun(2); // 1。“var b2 = b1.fun(1);”中,b2 中函数 fun 的作用域链中的 n 为 1,o 为 0。调用全局函数的 fun 时,n=(m=)2,o=(n=)1。因此打印内容为“1”。
var b4 = b3.fun(3); // 2。理由同上。
var c = fun(0).fun(1); // undefined,0
c.fun(2);// 1
c.fun(3);// 1
// 为了便于理解,我们将链式调用拆分以下等价的方案进行解释:
var c1 = fun(0); // undefined。这个和“var a = fun(0);”,不重复解释。
var c = c1.fun(1); // 0。要注意的是,“c1.fun(1);”返回的对象由变量 c 接收,即 c 中的函数 fun 作用域链中的变量:n=1,o=0。
c.fun(2);// 1。
c.fun(3);// 1。“c.fun(2);”中返回的对象不会影响到 c。因此此处和执行“c.fun(2);”时一样,c 中的函数 fun 作用域链并未被改变。
我们可以简单理解为:函数创建时,就已经根据上下文环境保存一套变量。当我们在调用闭包函数时,闭包函数自身不存在的变量,将会在这套变量中查找。
值得一提 1、“变量声明提升”对于闭包的实现是非常重要的。如果变量声明没有被提升,那么我们将无法保存那些在闭包函数创建以后才声明的变量。2、闭包的机制,作用域链会一直引用自身以外的函数的全部变量,内存回收机制不能及时回收这些变量,从而增大内存开销。