闭包(closure)是JavaScript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。
变量的作用域
要理解闭包,首先必须理解 JavaScript 特殊的变量作用域。
变量的作用域无非就是两种:全局和局部。
|
|
另一方面,在函数外部自然无法读取函数内的局部变量。
|
|
这里需要注意,函数内部声明变量的时候,一定要使用 let
(immutable
不可变对象可以使用 const
),而不是使用 var
或者不写。如果不使用 let 的话,你实际上声明了一个全局变量!
let
和 const
是 es6 标准引入的,这里就不建议使用老标准的 var 了,因为涉及到作用域提升的话题会耗费很多篇幅,初学者记住用 let
就对了。
|
|
什么是闭包
有时我们有时候需要使用函数内的局部变量,但是从函数作用域外部获取函数作用域内的变量是不可能的。
那么在函数的内部,再定义一个函数怎么样呢。
|
|
在上面的代码中,函数 bar
就被包括在函数 foo
内部,这时 foo
内部的所有局部变量,对 bar
都是可见的。但是反过来就不行, bar
内部的局部变量,对 foo
就是不可见的。这就是 JavaScript 语言特有的“作用域链(Scope Chain)”结构,子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。
不仅如此,既然 bar
可以读取 foo
中的局部变量,那么只要把 bar
函数作为返回值。
|
|
其实这里的 bar
函数,就是一个闭包函数。
各种专业文献上的"闭包"(closure)定义非常抽象。其实简单来说,闭包就是函数中的函数,能够读取其他函数内部变量的函数。
闭包的用途
闭包可以用在许多地方。它的最大用处有两个,一个是前面提到的可以读取函数内部的变量,另一个就是让这些变量的值始终保持在内存中。
怎么来理解这句话呢?请看下面的代码。
|
|
在这段代码中,fuga
实际上就是闭包 popup
函数。它一共运行了两次,第一次的值是114514,第二次的值是114515。这证明了,函数 foo
中的局部变量 n
一直保存在内存中,并没有在 foo
调用后被自动清除。
为什么会这样呢?原因就在于 foo
是 popup
的父函数,而 popup
被赋给了一个全局变量,这导致 popup
始终在内存中,而 popup
的存在依赖于 foo
,因此 foo
也始终在内存中,不会在调用结束后,被垃圾回收机制(Garbage collection)回收。
使用闭包的注意点
由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题。
闭包可以改变父函数局部变量的值。所以,如果你把父函数当作对象(Object)使用,把闭包当作它的公用方法(Public method),把内部变量当作它的私有属性(Private value),这时一定要小心,不要随便改变父函数内部变量的值。
思考题
如果你能理解下面两段代码的运行结果,应该就算理解闭包的运行机制了。
代码片段一。
|
|
代码片段二。
|
|
(参考:http://www.ruanyifeng.com/blog/2009/08/learning_javascript_closures.html,本文对其中有问题的点进行了修改。)