说到闭包就要提到函数,但是函数这一部分请自行查看文档 JavaScript 指南 函数 JavaScript 参考 函数
还要说到作用域和词法作用域, 什么是作用域? 词法作用域
定义
看了那么多,自己也在总结,主要有一下几个关键的点:
- 闭包是函数和声明该函数的词法环境的组合。
- 闭包就是函数能够记住并访问它的词法作用域,即使当这个函数在它的词法作用域之外执行时。
- JavaScript中的函数运行在它们被定义的词法作用域里,而不是它们被执行的词法作用域里。
总结成一句话是:
闭包是函数和声明该函数的词法环境的组合。而且函数能够记住并访问它的词法作用域,即使函数在它的词法作用域之外执行,也不会访问它们被执行的词法作用域。
(这里面使用的词法环境、词法作用域有待考证,因为原本只是认为是作用域,而作用域和词法作用域也可能是不同的)
特点:
- 实现了变量的私有化,隔离作用域不污染全局作用域。
- 不会被垃圾回收器回收作用域。
缺点:
- 增加了变量作用域链查找的长度,相对降低了性能。所以应该尽量减少闭包内变量查找作用域链的长度,或者不使用闭包。
解析
得见真容
1
2
3
4
5
6
7
8
9
10
11
12
13
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 2 -- 哇噢,看到闭包了,伙计。
函数 bar() 对于 foo() 内的作用域拥有词法作用域访问权。但是之后,我们拿起 bar(),这个函数本身,将它像 值 一样传递。在这个例子中,我们 return bar 引用的函数对象本身。
在执行 foo() 之后,我们将它返回的值(我们的内部 bar() 函数)赋予一个称为 baz 的变量,然后我们实际地调用 baz(),这将理所当然地调用我们内部的函数 bar(),只不过是通过一个不同的标识符引用。
bar() 被执行了,必然的。但是在这个例子中,它是在它被声明的词法作用域 外部 被执行的。
一般来说函数在执行完之后会被 引擎 启用了垃圾回收器 回收不被使用的作用域;而闭包不会,闭包会持续对这个作用域保持使用
循环 + 闭包
一个经典的错误
1 |
for (var i=1; i<=5; i++) { |
我们最初的设想是间隔1000毫秒分别输出 1 2 3 4 5,事实却是间隔1000毫秒分别输出 6 6 6 6 6。为什么会这样呢?
每一次执行setTimeout的回调函数都会产生一个闭包,但是这些闭包使用的是同一个词法作用域,而最后i变成了6,所以最后输出的都是6。解决这个问题还是有方法的。
以毒攻毒 - 用闭包解决:
1 |
function delay(i) { |
for 循环时会创建一个新的词法作用域,且多个词法作用域是互不干扰的。
也可以使用IIFE解决:
1 |
for (var i=1; i<=5; i++) { |
块作用域,使用 let 声明,这个变量将不是只为循环声明一次,而是为每次迭代声明一次。
1
2
3
4
5
for (let i=1; i<=5; i++) {
setTimeout( function timer(){
console.log( i );
}, i*1000 );
}
用闭包模拟私有方法
有些编程语言是支持私有方法的,而 JavaScript 没有这种原生支持,但我们可以使用闭包来模拟私有方法。
私有方法不仅仅有利于限制对代码的访问:还提供了管理全局命名空间的强大能力,避免非核心的方法弄乱了代码的公共接口部分。
1 |
var makeCounter = function(default) { |
我们定义了 makeCounter 函数工厂创建了两个新函数 — 一个将以2作为开始,另一个以10作为开始。
Counter1 和 Counter2 都是闭包。它们共享相同的函数定义,但是保存了不同的词法环境且不互相影响。
其实这又被称为模块。
模块
关于模块的部分模块化模式
参考
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Closures
https://www.zhihu.com/question/34547104
https://github.com/holidaypenguin/You-Dont-Know-JS/blob/1ed-zh-CN/scope%20%26%20closures/ch5.md