前端领域对于闭包有很多种解释。我认为的闭包应该是这样的:在一个局部作用域中定义了一个函数,这个函数访问了属于它外部作用域中的属性,并且当这个函数不在它被定义的地方调用时,就产生了闭包。看下面的例子:
1 | function outer () { |
这个例子会输出张三
。当 inner()
函数被调用时,需要输出变量 name
的值。编译器首先会在 inner()
自身的作用域中查找 name
属性,没有找到;然后在 inner()
的父级作用域,也就是 outer()
函数的作用域中查找,找到了,于是停止继续查找,并且将这个 name
属性的值输出。
那么上面这个例子产生闭包了吗?对于闭包有这样一种解释:
声明在一个函数中的函数,叫做闭包函数
以上面这种解释来看,本例中 inner()
函数就产生了一个闭包。但是我认为这并不是一个准确的说法。
尽管 inner()
函数是在 outer()
的作用域中被声明,并且引用了属于 outer()
作用域的属性 name
。但是,inner()
的是在它被定义的地方调用的,而并没有在它被定义之外的地方调用。也就是说,inner()
对于 name
的引用,是按照正常的作用域链规则进行的。在这个过程中,并没有闭包的参与,也就不可能产生闭包。
来看第二个例子:
1 | function outer () { |
这个例子中,浏览器同样会输出 张三
。与第一个例子不同的是,inner()
函数并没有在它被定义的地方调用,而是将 inner()
函数作为一个值,从 outer()
函数中返回了;然后在全局作用域中,定义了一个变量 fn
用来接收 outer()
函数的返回值,也就是 inner()
函数,此时,闭包产生了;随后,在全局作用域中调用了 inner()
函数,此时,闭包被使用了。
对 inner()
函数的调用过程做一些解释:inner()
函数被调用时,引用了 name
属性。编译器首先会在 inner()
函数自身作用域中查找 name
属性,没有找到;接下来,编译器并不会直接到 inner()
函数的外部作用域中查找 name
属性,而是在 inner()
函数的闭包中查找。而 inner()
函数的闭包中,正好就保存了 name
属性。
可以把闭包看作一个特殊的作用域,当 inner()
函数在作为值被返回时,由于它离开了定义时所在的作用域(被赋值给外部作用域的 fn
变量),于是它会将它所引用的外部属性保存在自身的闭包中。这样,当 inner()
函数被调用时,就能在它的闭包中找到原本应该在 outer()
作用域中的属性。
我认为 inner()
函数闭包中所保存的仅仅是它所引用的外部作用域中的属性,而不是将外部作用域整个保存。也就是说,出现在 inner()
函数的闭包空间中的仅仅是 inner()
所引用的 name
属性,而非整个 outer()
函数的作用域。
下面这个例子证明了, inner()
函数在查找不属于自身作用域的属性时,会首先查找闭包而不是外部作用域。
1 | let name = '李四' |
这里,在全局作用域中同样定义了 name
属性。不过,inner()
函数在创建自身的闭包作用域时(我认为闭包就是一个特殊的作用域),会将 outer()
函数的 name
属性保存进去(同样遵循了词法作用域的规则),并且在 inner()
被调用时同样先查找闭包作用域。
闭包有一个奇怪的现象:如果 inner()
函数并没有引用 outer()
函数的属性,而是引用的全局作用域中的属性,闭包就不会产生了。
1 | let name = '李四' |
这个例子中 inner()
函数并没有引用 outer()
函数的属性,而是引用了全局的 name
属性。这种情况下并没有产生闭包。然而下面的例子却产生了闭包:
1 | function outer () { |
与上一个例子不同,这个例子中产生了闭包。
我并不清楚具体是什么原因导致了这种区别,或许是因为浏览器对于函数的特殊处理,又或许是因为函数作用域与全局作用域之间存在着某种区别,总之这种区别确确实实存在。
闭包在实际开发过程中有着广泛的应用,在定时器、事件绑定、ajax请求以及模块中都能看到闭包的出现。下面有几个使用闭包的简单例子。
1 | function bindTimer () { |
1 | function bindClick () { |
1 | function createPerson () { |
1 | let person = (function () { |
总结:当一个函数定义在另一个函数内部,同时这个内部的函数引用了它的外部函数作用域中的属性,并且该函数被从它定义的地方传递到其他地方时,闭包就会被创建;随后当这个函数被调用时,闭包就会被使用。闭包可以看作这个函数的特殊作用域,包含了该函数引用的所有非全局的外部属性。