曾几何时,闭包好像就是一个十分难以捉摸透的东西,看了很多文章,对闭包都各有说法,以致让我十分晕,什么内部变量、外部变量的,而且大多数都只描述一个过程,没有给闭包的定义,最后,举几个例子,告诉你这就是闭包。于是乎,我从来都是带有疑问使用闭包的:闭包是指作用域,还是指函数,还是指访问外部变量的过程?还有外部变量有多外面?直到最近的学习,我才渐渐清晰......
前提
首先,我们需要知道 JavaScript 里面的函数会创建内部词法作用域,是的,JavaScript 是词法作用域,也就是说作用域与作用域的层级关系在你书写的时候就已经确定了,而不是调用的时候,调用的时候确定的称为动态作用域,由于不是本篇文章的重点,就不再详细解释了,举两个例子自己领悟:
var name = 'fruit'
function apple () {
console.log(name)
}
function orange () {
var name = 'orange'
apple()
}
orange() // fruit
由于 JavaScript 是词法作用域,所以 apple
函数的局部作用域的上层作用域是全局作用域,从书写的位置就看出来了。假设 JavaScript 是动态作用域,就要看函数的调用顺序了,由于 apple
是在 orange
中调用的,所以 apple
的上层作用域是 orange
的局部作用域,那样的话会输出 orange!
这样的话,就制定了一套作用域访问的规则,这也是会有闭包的原因之一!
什么是闭包?
函数记住并访问其所在的词法作用域,叫做闭包现象,而此时函数对作用域的引用叫做闭包。
当我看到这句话的时候,泪流满面,国外的作者就是一语道破真相。简单的说,闭包就是引用,对谁的引用呢,对作用域的引用,只不过这种引用是有条件的——首先要记住作用域,然后再访问作用域!
什么叫记住作用域?
首先,我们都知道,在 JavaScript 里面,如果函数被调用过了,并且以后不会被用到,那么垃圾回收机制就会销毁由函数创建的作用域,我们还知道,对象(函数也是对象)的传递属于传引用,也就是类似于C语言里面的指针,并不会把真正的值拷贝给变量,而是把对象所在的位置传递给变量,所以,当函数被传引用到一个还未销毁的作用域的某个变量,由于变量存在,所以函数得存在,又因为函数的存在依赖于函数所在的词法作用域,所以函数所在的词法作用域也得存在,这样一来,就记住了该词法作用域。也就解释了该节的标题!下面举个例子说明一下:
// 没有闭包现象的时候
function apple () {
var count = 0
function output () {
console.log(count)
}
fruit(output)
}
function fruit (arg) {
console.log('fruit')
}
apple() // fruit
当我们在调用 apple
的时候,本来 apple
在执行完毕之后 apple
的局部作用域就应该被销毁,但是由于 fruit(output)
将 output
传引用给了 arg
,所以在 fruit
执行的这段时间内,arg
肯定是存在的,被引用的函数 output
也得存在,而 output
依赖的 apple
函数产生的局部作用域也得存在,这就是所谓的“记住”,把作用域给记住了!
但是,上面的例子是闭包现象吗?不是,因为函数 output
内部并没有访问记住的词法作用域的变量!在执行 fruit(output)
的过程中,只发生了 arg = output
的传引用赋值,而这个过程,只是把二者关联起来了,并没有去取 arg
引用的对象的值,所以实际上也并没有访问 output
所在的词法作用域!
记住并访问
上面的代码,稍微修改一下就会产生闭包现象了:
function apple () {
var count = 0
function output () {
console.log(count)
}
fruit(output)
}
function fruit (arg) {
arg()
}
apple() // 0
现在,调用 fruit
时,apple
的局部作用域处于“记住”的状态,这时候, fruit
内部调用了 arg()
,因为传引用,实际上访问并执行了 apple
局部作用域的 output
,不仅仅是这样,output
内部还访问了 count
变量,这两次对 apple
局部作用域的引用都是闭包!
所以,之所以说所有回调函数的调用都会产生闭包现象,也是因为这个回调函数被传给了另外一个函数的参数,所以在另外一个函数的作用域消失之前,回调函数所在的词法作用域都被记住了,由于回调函数一定会被执行,所以回调函数所在的词法作用域至少被访问了一次,也就是至少访问回调函数本身,而这个对作用域的引用就是闭包。
闭包的作用
根据上面的讲解,估计你自己都能倒背如流了:
记住了函数所在的词法作用域,使其不被销毁;
能够访问函数所在词法作用域的变量;
创建模块(设计私有变量、公有函数等等)
还有很多,就不一一说了,下面就是利用闭包来解决一个常见的问题:
for (var i = 0; i < 5; i++) {
// 为了方便说明,给函数起名叫 apple
setTimeout(function apple () {
console.log(i) // 5 个 5
}, 0)
}
首先读者们先思考一个问题,这会产生闭包吗?
其实,上面也也会产生闭包,只不过 apple
记住并访问的是全局作用域,为什么呢?因为回调函数被当做 setTimeout
的参数传引用过去了,假设 setTimeout
实现如下
var setTimeout = function (callback, delay) {
// 延迟
callback()
}
看到没,因为 setTimeout
属于异步函数,所以会等到 JS 执行完毕之后再调用 callback
,所以这段时间 callback
一直存在,所以函数 apple
也一直存在,所以全局作用域并不会等 JavaScript 执行完毕后就销毁(函数 apple
属于全局作用域的),这时候循环早结束了,所以 i
也变成了 5,于是乎,这个时候 apple
对全局作用域的引用称为闭包!
上面也说了回调函数调用都会产生闭包,这里就当举例说明一下!
那么怎么解决以上问题呢,很简单,让回调函数记住不同的作用域就行了!
for (var i = 0; i < 5; i++) {
// 为了方便说明,给函数起名叫 apple
(function baz (i) {
setTimeout(function apple () {
console.log(i)
}, 0)
})(i) // 0 1 2 3 4
}
上面用立即执行函数解决了问题,因为函数有局部作用域,所以调用 5 次函数会产生 5 个局部作用域,每个作用域的 i
由各次循环的 i
传递赋值,而每个作用域内都存在 apple
,都记住了各自的作用域,也就取到了不同的 i
!
不过通常来说,闭包都是按以下方式产生:
function apple () {
var name = 'apple'
var output = function () {
console.log(name)
}
return output
}
var out = apple()
out() // apple
上述将函数传引用给了全局作用域的变量,显然,闭包(对 apple
作用域的引用)在全局作用域都存在的情况下都可能发生,而且后面也执行了 out()
!
更常见的写法是下面这种:
function Apple () {
var name = 'apple'
var output = function () {
console.log(name)
}
var setName = function (arg) {
name = arg
}
return {
output: output,
setName: setName
}
}
var apple = Apple()
apple.output() // apple
apple.setName('Apple')
apple.output() // Apple
这就是模块的一个例子,name
通常被称为私有变量!
结语
闭包没什么了不起的,这是被人玩的过于玄乎,其实这是人们很自然的想法:我在别的地方调用函数,总得保持函数正常运行吧!“闭包”这种机制很轻松的帮你解决了这个问题,我们不必搞懂闭包是什么也经常在实现它(如果这句话写在前面,会不会很多人都不看了,哈哈),这是语言设计者的过人之处,但是,你不搞懂它,总被人质疑:你不懂闭包吧!实际上,我们都实现了很多次闭包,所以,你把内部机制详细搞清楚了,就不会再害怕别人的质疑了,哈哈!当然,如果你喜欢钻研,更有必要了解其中的机制了,体会到寻找语言设计者设计思路的快感!
最后,再总结一下:函数记住并访问其所在的词法作用域,叫做闭包现象,而此时函数对作用域的引用叫做闭包。
最后的最后,再强调一下:闭包就是引用!
更多文章请看我的个人博客
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。