弄懂Javascript闭包
写在前面
闭包(closure)是JavaScript中的一个非常重要的特性,在 JavaScript 版本的设计模式中,许多模式都可以用闭包和高阶函数来实现。
对于 JavaScript 程序员来说,闭包(closure) 是一个难懂又必须征服的概念。
维基百科有这么一段话
在计算机科学中,闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数。
这个被引用的自由变量将和这个函数一同存在,即使已经离开了创造它的环境也不例外。
所以,有另一种说法认为闭包是由函数和与其相关的引用环境组合而成的实体。闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。
1
2
3
4
5
6
7
8
9
10
11
12
13 function test(x){
var y = 10;
return function(){
var z = x + y;
console.log(x, y, z);
}
}
var a = test(1);
var b = test(2);
a(); // 1 10 11
b(); // 1 10 12闭包的概念出现于60年代,最早实现闭包的程序语言是Scheme。之后,闭包被广泛使用于函数式编程语言如ML语言和LISP。很多命令式程序语言也开始支持闭包。
在一些语言中,在函数中可以(嵌套)定义另一个函数时,如果内部的函数引用了外部的函数的变量,则可能产生闭包。运行时,一旦外部的 函数被执行,一个闭包就形成了,闭包中包含了内部函数的代码,以及所需外部函数中的变量的引用。其中所引用的变量称作上值(upvalue)。
闭包一词经常和匿名函数混淆。这可能是因为两者经常同时使用,但是它们是不同的概念。
闭包的形成
闭包的形成与变量的作用域以及变量的生存周期密切相关
变量的作用域
变量的作用域,就是指变量的有效范围。我们最常谈到的是在函数中声明的变量作用域。
当在函数中声明一个变量的时候,如果该变量前面没有带上关键字var,这个变量就会成为全局变量,这当然是一种容易造成命名冲突的做法。
另外一种情况是用var关键字在函数中声明变量,这时候的变量即是局部变量,只有在该函数才能访问到这个变量,在函数外面是访问不到的。代码如下:
1 | var func = function(){ |
在 JavaScript 中,函数可以用来创造函数作用域。此时的函数像一层半透明的玻璃,在函数里面可以看到外面的变量,而在函数外面则无法看到函数里面的变量。这是因为当在函数中搜索一个变量的时候,如果该函数内并没有声明这个变量,那么此次搜索的过程会随着代码执行环境创建的作用域链往外层逐层搜索,一直搜索到全局对象为止。变量的搜索是从内到外而非从外到内的。
1 | var a = 1; |
变量的生存周期
除了变量的作用域之外,另外一个跟闭包有关的概念是变量的生存周期。
对于全局变量来说,全局变量的生存周期当然是永久的,除非我们主动销毁这个全局变量。
而对于在函数内用var关键字声明的局部变量来说,当退出函数时,这些局部变量即失去了它们的价值,它们都会随着函数调用的结束而被销毁:
1 | var func = function(){ |
现在来看看下面的这段代码:1
2
3
4
5
6
7
8
9
10
11var func = function(){
var a = 1;
return function(){
a++;
alert(a);
}
}
var f = func();
f();
f();
f();
跟我们之前的推论相反,当推出函数后,局部变量a并没有消失,而是似乎一直在某个地方存在。
这是因为当执行了 var f = func()
时,f
返回了一个匿名函数的引用,它可以访问到func()
被调用时产生的环境,而局部变量a
一直处在这个环境里。既然局部变量所在的环境还能被外界访问,这个局部变量就不会被垃圾回收机制销毁。在这里产生了一个闭包结构,局部变量的生命看起来被延续了。
总结
闭包(英语:Closure),又称词法闭包(Lexical Closure)或函数闭包(function closures),是引用了自由变量的函数,只要这个函数正在被使用或者将来可能会被使用,那么这些被引用的自由变量就不会被销毁,这就形成了闭包