闭包在JavaScript中是让很多人在他们脑中挣扎的概念之一。接下来的文章中,我将清楚的解释什么是闭包,同时我会使用简单的代码示例来直接说明。
什么是闭包?
闭包在JavaScript中是一个特性,是一个作用域链,是内部函数可以访问外部函数的变量。
闭包有三种作用域链:
- 它可以访问自己的作用域——在它自己花括号中定义的变量
- 它可以访问外部函数的作用域
- 它可以访问全局变量
对于门外汉来说,这个定义看起来像全部术语了!
但是什么是真正的闭包?
一个简单的闭包
来看看JavaScript中的简单闭包:
function outer() {
var b = 10;
function inner() {
var a = 20;
console.log(a+b);
}
return inner;
}
这里我们有两个方法:
- 外部的
outer函数有一个变量b,然后返回了inner方法 - 内部的
inner函数有它自己的变量a,同时在它函数内部可以访问outer的变量b
b的作用域对outer函数是受限的,同时a的作用域对inner函数是受限的。
现在调用outer()方法,然后把outer()的结果保存在变量x中。再调用outer()方法把结果存储在Y中。
function outer() {
var b = 10;
function inner() {
var a = 20;
console.log(a+b);
}
return inner;
}
var X = outer(); // outer() invoked the first time
var Y = outer(); // outer() inovked the second time
让我们一步步来看当 outer() 被第一次调用的时候发生了什么:
- 变量
b被创建,它的作用域被限制在outer()方法中,同时它的值被设置为10。 - 下一行是函数声明,没有什么执行。
- 在最后一行,
return inner看起来是一个叫inner的变量,找到它发现这个变量inner实际上是一个方法,所以它返回了整个inner的函数体。 【注意这个return语句并没有执行这个inner 方法——一个方法被执行只有通过跟一个()——,而不是这个return声明返回整个函数体】 - 通过返回声明的年内容被存储在了X中。因此
x保存了以下内容:function inner() { var a = 20; console.log(a+b); } outer()方法执行完成,同时在outer()作用域内的所有变量都不再存在。
最后一部分对于理解至关重要。一旦一个函数完成了执行,在函数作用域中内部定义的的任何变量不在存在。
定义在函数内部的变量的的有效期就是函数执行的有效期。
console.log(a+b) 是什么意思,变量 b 只存在于函数 outer() 执行期间。一旦 outer() 函数执行完成,变量 b 就不在存在。
当函数第二次执行的时候,函数的变量被再次创建,而且直到函数完全执行一直存在。
因此,当 outer() 被再次调用:
- 一个新的变量
b被创建,它的作用域限制在函数outer()中,同时它的值被设置为10. - 下一行是函数声明,没有做什么事。
return inner返回了函数inner的整个函数体- 通过返回语句返回的内容储存在
Y中。 - 函数
outer()执行结束,outer()作用域中的所有变量不再存在
这里有个重要的点,当 outer() 方法被第二次调用的时候,变量 b 是全新的。同样,当 outer() 方法第二次执行结束后,这个新变量 b 再次消失。
这是要意识到的最重要的点。函数内部的变量只存在于一个函数运行时,同时在函数执行完毕后立即消失。
现在,来回到示例代码去看看 X 和 Y。 既然 outer() 函数返回了一个函数,变量 X 和 Y 都是函数。
下面的JavaScript代码很容易证实:
console.log(typeof(X)); // X is of type function
console.log(typeof(Y)); // Y is of type function
因为变量 X 和 Y 都是函数,我们可以执行他们。在JavaScript中,方法执行可以通过在方法名后面添加 (),比如 X() 和 Y()。
function outer() {
var b = 10;
function inner() {
var a = 20;
console.log(a+b);
}
return inner;
}
var X = outer();
var Y = outer();
//end of outer() function executions
X(); // X() invoked the first time
X(); // X() invoked the second time
X(); // X() invoked the third time
Y(); // Y() invoked the first time
当我们执行 X() 或者 Y() 的时候,我们实际上在执行 inner 函数。
让我们来一步步看看当第一次执行 X() 时发生了什么:
- 变量
a创建,它的值是20。 - JavaScript尝试执行
a+b。这就是有趣的地方。JavaScript知道a存在因为刚刚创建了它。然而b不再存在。因为b是外部方法的一部分,b只有当outer()执行的时候才存在。因为在我们调用X()之前,outer()函数已经执行完了,任何在outer函数中的变量停止存在,因此变量b不再存在。
JavaScript如何处理?
闭包
由于JavaScript中的闭包,inner 函数可以访问包裹函数的变量。换句话说, inner 函数在包裹函数执行的时候保留了包裹函数的作用域,因此可以访问包裹函数的变量(译者注:函数里面包一个函数,外部的函数就叫做包裹函数)。
在我们的例子中,当 outer 函数执行时,inner 函数保留了变量 b=10,同时继续保留(闭包)它。
现在来看看它的作用域链,注意在它的作用域链中有一个变量 b,因为当 outer 函数执行时,它在那时已经在一个闭包中包裹了变量 b。
因此,JavaScript知道 a=20 和 b=10, 就可以计算 a+b。
你可以通过给上面的例子添加以下代码来证明:
function outer() {
var b = 10;
function inner() {
var a = 20;
console.log(a+b);
}
return inner;
}
var X = outer();
console.dir(X); //use console.dir() instead of console.log()
打开Chrome的元素检查,找到 Console。你可以展开这个元素看看真正的 Closure 元素(从最后一行数第三个)。注意这个变量 b=10 被保留在闭包中,即使 outer() 函数完成了它的执行。
现在重新回顾一下闭包的定义,我们最开始看到过,现在来看更有意义。
那么内部函数有三个作用域链:
- 可以访问它自己的作用域——变量
a - 可以访问
outer函数的变量b,它是封闭的。 - 可以访问任何定义的全局变量
闭包的行为
直接来看闭包,我们来通过三行代码加强下面那这个例子:
function outer() {
var b = 10;
var c = 100;
function inner() {
var a = 20;
console.log("a= " + a + " b= " + b);
a++;
b++;
}
return inner;
}
var X = outer(); // outer() invoked the first time
var Y = outer(); // outer() invoked the second time
//end of outer() function executions
X(); // X() invoked the first time
X(); // X() invoked the second time
X(); // X() invoked the third time
Y(); // Y() invoked the first time
当你运行这段代码,你会在 console.log 的输出中看到如下:
a=20 b=10
a=20 b=11
a=20 b=12
a=20 b=10
来一步步检查这段代码看看究竟发生了什么,和闭包的行为!
var X = outer(); // outer() invoked the first time
函数 outer() 第一次被调用。发生下面几步:
-
变量
b被创建,赋值10 -
变量
c被创建,赋值100我们称
b(first_time)和c(first_time)作为自己的参考。inner函数返回并赋值给x。在这一点上,变量b被带着b=10的闭包的inner函数的作用域链包裹着,因此inner使用了变量b。 -
outer函数完全执行完成,所有的变量不再存在。变量c不在存在,尽管变量b存在于inner的闭包中。
var Y= outer(); // outer() 第二次调用
-
b被重新创建,赋值为10c被重新创建。注意即使
outer()在变量b和c消失前就执行一次,一旦函数完全执行他们就像被新创建的变量一样。我们称
b(second_time)c(second_time)作为参考。 -
inner函数返回并且给Y赋值 在这一点上,变量b是被作为带有b(second_time)=10的闭包的inner函数的作用域链包裹着。因此,inner可以使用变量b。 -
outer函数完成执行后,它的变量不再存在。变量
c(second_time)不再存在,尽管变量b(second_time)作为闭包存在于inner中。
现在来看看当以下代码执行后发生了什么:
X(); // X() invoked the first time
X(); // X() invoked the second time
X(); // X() invoked the third time
Y(); // Y() invoked the first time
当 x() 第一次调用时:
-
变量
a被创建,赋值20 -
a=20是结果,b的值来自闭包的值。b(first_time),所以b=10。 -
变量
a和b被加1 -
x()完成执行,它内部的变量——变量a——消失。
当 x() 第二次调用:
-
变量
a被再次创建,设置为20变量
a之前的任何值不再存在,因为当x()第一次完全执行后它被消除。 -
变量
a=20变量
b取自闭包中的值——b(first_time)也要注意我们已经从之前的执行中给
b的值增加了1,所以b=11。 -
变量
a和b再次增加1 -
x()方法执行完成,然后所有它内部的变量都被消除然而,
b(first_time)被保留下来作为闭包继续存在。
当 x() 第三次调用的时候:
-
变量
a被重新创建,赋值20变量
a之前的任何值都不再存在,因为当x()第一次执行完成后就被消除。 -
现在的值
a=20,b的值来自于闭包的值——b(first_time)同时注意,我们在之前的执行中给值增加了
1,所以b=12 -
变量
a和b再次加1 -
x()完成执行,同时它所有的内部变量——变量a——擦除。然而,
b(first_time)被作为闭包继续保留了下来。
当 Y()第一次被调用的时候,
-
变量
a被重新创建,赋值20 -
此时的值
a=20,b的值来自闭包的值——b(second_time),所以b=10 -
变量
a和b增加1 -
Y()完成执行,所有的内部变量——变量a——被清除然而,
b(second_time)作为闭包被保留下来,所以b(second_time)继续存在。
总结
闭包在JavaScript中不易察觉,刚开始也难理解。但是一旦理解它们,你会意识到这是一件在其他地方不存在的事。
希望这些逐步解释能帮助你真正理解JavaScript的闭包概念。