# 什么是闭包(Closure)
在《JavaScript高级程序设计》里,有以下定义:
闭包指有权访问另一个函数作用域中变量的函数。
首先来看下列代码
1 | function foo() { |
在这个例子中,函数 bar()
的词法作用域能够访问 foo()
的内部作用域。然后我们将 bar()
函数本身当作一个值类型进行传递。
在 foo()
执行后,其返回值(也就是内部的 bar()
函数)赋值给变量 func
并调用 func()
,实际上只是通过不同的标识符引用调用了内部的函数 bar()
。
显然bar()
会被正常执行。更进一步地说,它是在自己定义的词法作用域以外的地方执行。
在 JavaScript 中,因为有垃圾回收器的存在,因此在一个函数被执行后,如果检测到他的内容在之后不会再被使用,那么引擎会考虑对其进行回收。(对于 JavaScript 的垃圾回收,可以参考这篇文章)
然而在这个例子中,foo()
被执行后,事实上内部作用域依然存在,因此没有被回收。谁在使用这个内部作用域?是 bar()
本身在使用。
拜 bar()
所声明的位置所赐,它拥有涵盖 foo()
内部作用域的闭包,使得该作用域能够一直存活,以供 bar()
在之后任何时间进行引用。
换句话说,bar()
持有对该作用域的引用,而这个引用就叫作闭包。
因此,在几微秒之后变量 func
被实际调用,不出意料它可以访问定义时的词法作用域,因此它也可以如预期般访问局部变量 local
。
这几行代码可能过于复杂,我们可以把它精简成这样:
1 |
|
在这三行代码中,bar
可以访问到 local
变量,这就是一个闭包。诚然如此,但我们通常会把这三行代码放在一个函数里。为什么呢?这就涉及到闭包的作用了。
# 闭包的作用
闭包通常用来间接访问一个变量。也就是说,可以隐藏一个变量使它不能被直接访问。
要想达到这个效果,就可以把这个变量放在一个作用域内,然后单独创建一个对他进行控制的函数,这样我们就只能通过这个函数去访问它。
这个函数在定义时的词法作用域以外的地方被调用。闭包使得函数可以继续访问定义时的词法作用域。
无论通过何种手段将内部函数传递到所在的词法作用域以外,它都会持有对原始定义作用
域的引用,无论在何处执行这个函数都会使用闭包。
# 一些理解
无论何时何地,如果将函数(访问它们各自的词法作用域)当作第一级的值类型并到处传递,就会产生闭包。
在定时器
、事件监听器
、Ajax 请求
、跨窗口通信
、Web Workers
或者任何其他的异步或者同步任务中,只要使用了回调函数,实际上也是在使用闭包。
比如下列代码:
1 | for (var i = 1; i <= 5; i++) { |
正常情况下,我们对这段代码行为的预期是依次输出数字 1~5,每秒一次输出一个。但实际上,这段代码在运行时会以每秒一次的频率输出五次 6。
事实上,当定时器运行时即使每个迭代中执行的是 setTimeout(.., 0)
,所有的回调函数依然是在循环结束后才会被执行,因此会每次输出一个 6 出来。
但是根据作用域的工作原理,尽管循环中的五个函数是在各个迭代中分别定义的,但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个 i
。也就是说,所有函数都共享一个 i
的引用。
而这个循环的终止条件是 i > 5
。条件首次成立时 i
的值是 6。因此,输出显示的是循环结束时 i
的最终值。
那么怎么解决呢?
我们可以使用 IIFE
(Immediately Invoked Function Expression)即立即调用函数来对 i
创建一个单独的作用域,并使用另一个变量 j
在每次迭代中存放 i
的值,代码如下:
1 | for (var i = 1; i <= 5; i++) { |
这样代码就能按照预期输出 1~5 了。