这篇文章使用一些简单的代码例子来解释JavaScript闭包的概念,即使新手也可以轻松参透闭包的含义。
其实只要理解了核心概念,闭包并不是那么的难于理解。但是,网上充斥了太多学术性的文章,对于新手来说,看完这些文章可能会更加一头雾水。
这篇文章面向的是使用主流开发语言的程序员,如果你能读懂下面这段代码,恭喜你,你可以开始JavaScript闭包的学习之旅了。
1
2
3
4
5
6
7
8
|
function
sayHello
(
name
)
{
var
text
=
'Hello'
+
name
;
var
say
=
function
(
)
{
console
.
log
(
text
)
;
}
say
(
)
;
}
sayHello
(
'Joe'
)
;
|
我相信你一定看懂了,那我们就开始吧!
举例之前,我们先用两句话概括一下:
下面的例子返回了对一个方法的引用:
1
2
3
4
5
6
7
8
9
|
function
sayHello2
(
name
)
{
var
text
=
'Hello'
+
name
;
//局部变量
var
say
=
function
(
)
{
console
.
log
(
text
)
;
}
return
say
;
}
var
say2
=
sayHello2
(
'Bob'
)
;
say2
(
)
;
//logs='Hello Bob'
|
我想大多数JavaScript程序员都能理解上面代码中一个函数的引用是如何被赋值给一个变量(say2)的。如果你不清楚的话,最好在继续了解闭包之前弄清楚。使用C语言的程序员或许会认为这个函数是指向另一个函数的指针,并且变量say和say2也同样是指向函数的指针。
然而C语言中指向函数的指针和JavaScript中对一个函数的引用有很大的不同。在JavaScript中,你可以把引用函数的变量当作同时拥有两个指针:一个指向函数,另一个隐形地指向闭包。
上面的代码中生成了一个闭包是因为匿名函数function(){console.log(text);}被定义在了另外一个函数sayHello2()中。在JavaScript中,如果你在一个函数中定义了另外一个函数,那么你就创建了一个闭包。
在C语言或者其他流行的开发语言当中,函数返回之后,所有局部变量都不能再被访问,因为栈帧已经被销毁了。
在JavaScript中,如果在一个函数中定义了另外一个函数,即使从被调用的函数中返回,局部变量依然能够被访问到。正如上面例子中我们在得到sayHello2()的返回值之后又调用了say2()一样。需要注意到,我们调用的代码中引用了函数sayHello2()中的局部变量text。
1
|
[
crayon
-
5a96728f20f4c096843270
class
=
"javascript"
]
<
span
class
=
"hljs-function"
>
<
span
class
=
"hljs-keyword"
>
function
<
/
span
>
(
)
<
/
span
>
{
<
span
class
=
"hljs-built_in"
>
console
<
/
span
>
.
log
(
text
)
;
}
<
span
class
=
"hljs-comment"
>
//say2.toString()的输出结果;
|
[/crayon]
观察say2.toString()的输出结果,我们会发现代码指向变量text。这个匿名函数能够引用值为Hello Bob的变量text是因为sayHello2()的局部变量被保留在了闭包中。
在JavaScript中神奇的地方在于引用一个函数的同时会有一个秘密的引用指向在这个函数内部创建的闭包,类似于委托一个方法指针加一个隐藏的对象引用。
当你读到很多关于闭包的文章时,总会感觉一头雾水,但是当你看到一些应用的例子时,你就能清晰的理解闭包是如何工作的了。下面是我推荐的一些例子,希望大家能够认真研究直到真正清楚闭包是如何工作的。如果在你没有完全理解的情况下就开始使用闭包,你很快就会成为很多奇怪bug的创造者。
下面这个例子展示了局部变量不是被复制,而是被保留在了引用当中。这是当外部函数存在的情况下将栈帧保存在内存中的方法之一。
1
2
3
4
5
6
7
8
9
|
function
say667
(
)
{
//处于闭包中的局部变量
var
num
=
42
;
var
say
=
function
(
)
{
console
.
log
(
num
)
;
}
num
++
;
return
say
;
}
var
sayNumber
=
say667
(
)
;
sayNumber
(
)
;
//logs 43
|
下面例子中的三个全局函数有对同一个闭包的共同引用,因为他们都在setupSomeGlobals()中被定义。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
var
gLogNumber
,
gIncreaseNumber
,
gSetNumber
;
function
setupSomeGlobals
(
)
{
//处于闭包中的局部变量
var
num
=
42
;
// 用全局变量存储对函数的引用
gLogNumber
=
function
(
)
{
console
.
log
(
num
)
;
}
gIncreaseNumber
=
function
(
)
{
num
++
;
}
gSetNumber
=
function
(
x
)
{
num
=
x
;
}
}
setupSomeGlobals
(
)
;
gIncreaseNumber
(
)
;
gLogNumber
(
)
;
// 43
gSetNumber
(
5
)
;
gLogNumber
(
)
;
// 5
var
oldLog
=
gLogNumber
;
setupSomeGlobals
(
)
;
gLogNumber
(
)
;
// 42
oldLog
(
)
// 5
|
当这三个函数被创建时,它们能够共享对同一个闭包的访问-即对setupSomeGlobals()中的局部变量的访问。
需要注意到在上述例子中,如果你再次调用setupSomeGlobals(),会创建一个新的闭包。gLogNumber()、gSetNumber()和gLogNumber()会被带有新闭包的函数重写(在JavaScript中,当在一个函数中定义另外一个函数时,重新调用外部函数会导致内部函数被重新创建)。
下面这个例子对很多人来说都难以理解,所以你更需要真正理解它。在循环中定义函数时要格外小心:闭包中的局部变量或许不会和你的预想的一样。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
function
buildList
(
list
)
{
var
result
=
[
]
;
for
(
var
i
=
0
;
i
<
list
.
length
;
i
++
)
{
var
item
=
'item'
+
i
;
result
.
push
(
function
(
)
{
console
.
log
(
item
+
' '
+
list
[
i
]
)
}
)
;
}
return
result
;
}
function
testList
(
)
{
var
fnlist
=
buildList
(
[
1
,
2
,
3
]
)
;
for
(
var
j
=
0
;
j
<
fnlist
.
length
;
j
++
)
{
fnlist
[
j
]
(
)
;
}
}
testList
(
)
//logs "item2 undefined" 3次
|
注意到result.push( function() {console.log(item + ‘ ‘ + list[i])}向result数组中插入了三次对匿名函数的引用。如果你对匿名函数不太熟悉,可以想象成下面的代码:
1
2
|
pointer
=
function
(
)
{
console
.
log
(
item
+
''
+
list
[
i
]
)
}
;
result
.
push
(
pointer
)
;
|
需要注意到,当你运行上面的例子时,item2 undefined被打印了三次!这是因为像前一个例子中提到的,buildList的局部变量只有一个闭包。当在fnlist[j]()中调用匿名函数时,它们用的都是同一个闭包,而且在这个闭包中使用了i和item的当前值(i的值为3因为循环已经结束,item的值为item2)。因为我们从0开始计数所以item的值为item2,而i++会使i的值变为3。
下面这个例子展示了闭包在退出之前包含了外部函数中定义的任何局部变量。注意到变量alice其实是在匿名函数之后定义的。匿名函数先定义,但是当它被调用时它能够访问alice,因为alice和匿名函数处于同一作用域(JavaScript会进行变量提升)。sayAlice()()只是直接调用了sayAlice()返回的函数引用-但结果却和之前一样,只不过没有临时变量而已。
1
2
3
4
5
6
|
function
sayAlice
(
)
{
var
say
=
function
(
)
{
console
.
log
(
alice
)
;
}
var
alice
=
'Hello Alice'
;
return
say
;
}
sayAlice
(
)
(
)
;
// logs "Hello Alice"
|
注意到变量say也在闭包中,能够被任何在sayAlice()中定义的函数访问,或者在内部函数中被递归调用。
最后一个例子展现了每次调用都为局部变量创建一个独立闭包。不是每个函数定义都会有一个闭包,而是每次函数调用产生一个闭包。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
function
newClosure
(
someNum
,
someRef
)
{
var
num
=
someNum
;
var
anArray
=
[
1
,
2
,
3
]
;
var
ref
=
someRef
;
return
function
(
x
)
{
num
+=
x
;
anArray
.
push
(
num
)
;
console
.
log
(
'num: '
+
num
+
'; anArray: '
+
anArray
.
toString
(
)
+
'; ref.someVar: '
+
ref
.
someVar
+
';'
)
;
}
}
obj
=
{
someVar
:
4
}
;
fn1
=
newClosure
(
4
,
obj
)
;
fn2
=
newClosure
(
5
,
obj
)
;
fn1
(
1
)
;
// num: 5; anArray: 1,2,3,5; ref.someVar: 4;
fn2
(
1
)
;
// num: 6; anArray: 1,2,3,6; ref.someVar: 4;
obj
.
someVar
++
;
fn1
(
2
)
;
// num: 7; anArray: 1,2,3,5,7; ref.someVar: 5;
fn2
(
2
)
;
// num: 8; anArray: 1,2,3,6,8; ref.someVar: 5;
|
如果你对于闭包的概念依然不清晰,那么最好的方式就是运行一下上面的例子,看看会发生什么。读懂一篇长篇大论要比理解一个例子难的多。我对与闭包和栈帧的解释在技术上并不完全正确-而是为了帮助理解而简化了。如果这些基本点都掌握之后,你就可以朝着更细微之处进发了。
最后总结几点: