翻译:疯狂的技术宅原文:https://whatthefork.is/closure
未经允许严禁转载
闭包是令人困惑的,因为它是一个“无形的”概念。
当使用对象、变量或函数时,你会想:“在这里我需要一个变量”,然后将其添加到你的代码中。
闭包有各种不同的形式。很多人在注意到闭包时,实际上他们已经在不知不觉中多次使用过了——可能你也是如此。所以学习闭包不是要去了解什么新概念,而是要了解你已经接触过的东西。
太长不看版
当函数访问在其外部定义的变量时,你需要闭包。
例如,这段代码包含一个闭包:
let users = ['Alice', 'Dan', 'Jessica'];
let query = 'A';
let user = users.filter(user => user.startsWith(query));
注意 user => user.startsWith(query)
本身是一个函数。它使用了 query
变量。但是,query
变量是在该函数的“外部”定义的。那就是闭包。
如果你愿意,可以在这里就停止阅读。本文的其余部分会以不同的方式去处理闭包,并不解释闭包是什么,而是带你完成发现闭包的过程——就像1960年代的第一批程序员所做的那样。
第1步:函数可以访问外部变量
要了解闭包,我们需要对变量和函数有所了解。在这个例子中,我们在 eat
函数中声明了 food
变量。
function eat() {
let food = 'cheese';
console.log(food + ' is good');
}
eat(); // => 'cheese is good'
但是,如果我们以后想更改 eat
函数的 food
变量,该怎么办?为此,我们可以将 food
变量本身从函数中移到顶层:
let food = 'cheese'; // 我们把它移动到外部
function eat() {
console.log(food + ' is good');
}
这样我们可以在任何有需要的时候“从外部” 修改 food
:
eat(); // => 'cheese is good'
food = 'pizza';
eat(); // => 'pizza is good'
food = 'sushi';
eat(); // => 'sushi is good'
换句话说,food
变量不再是 eat
函数的局部变量,但是 eat
函数仍然可以轻松访问它。 函数可以访问它们之外的变量。先停下来想一秒钟,确保你对这个想法没有任何疑问。然后继续执行第二步。
第2步:在函数调用中包装代码
假设我们有一些代码:
/* 一些代码片段 */
这些代码做什么无关紧要。但是,假设我们要运行两次。
一种方法是复制并粘贴:
/* 一些代码片段 */
/* 一些代码片段 */
另一种方法是使用循环:
for (let i = 0; i < 2; i++) {
/* 一些代码片段 */
}
第三种方法,也是我们今天特别感兴趣的一种方法,将其包装在一个函数中:
function doTheThing() {
/* 一些代码片段 */
}
doTheThing();
doTheThing();
函数为我们提供了很大的灵活性,因为我们可以随时在程序中的任何位置把这个函数执行任意次。
如果愿意,我们也可以只调用一次:
function doTheThing() {
/* 一些代码片段 */
}
doTheThing();
请注意,上面的代码与原始代码段是等效的:
/* 一些代码片段 */
换句话说,如果我们有一段代码,将代码“包装”到一个函数中,然后只调用一次,那么我们就不会改变代码的作用。我们会忽略此规则的一些例外,但总的来说这应该是有道理的。停留在这个想法上,直到你的大脑完全理解为止。
第3步:发现闭包
前面我们通过两种不同的想法进行了探索:
- 函数可以访问在其外部定义的变量。
- 在函数中包装代码并调用一次不会改变结果。
那么如果把它们结合在一起会发生些什么呢。
我们将从第一步的代码开始:
let food = 'cheese';
function eat() {
console.log(food + ' is good');
}
eat();
然后,将整个例子中的代码包装到一个函数中,该函数将被调用一次:
function liveADay() {
let food = 'cheese';
function eat() {
console.log(food + ' is good');
}
eat();
}
liveADay();
再次审视两个代码片段,并确保它们是等效的。
这段代码有效!但是仔细看,注意 eat
函数在 liveADay
函数的内部。这允许吗?我们真的可以将一个函数放在另一个函数中吗?
在某些语言中,用这种方式写出来的代码是无效的。例如这种代码在 C 语言(没有闭包)中无效。这意味着在 C 语言中,前面的第二个结论是不正确的——我们不能随随便便就把一些代码包装在函数中。但是 JavaScript 不受这种限制。
再看这段代码,并注意在哪里声明和使用了 food
:
function liveADay() {
let food = 'cheese'; // 声明 `food`
function eat() {
console.log(food + ' is good'); // 使用 `food`
}
eat();
}
liveADay();
让我们一起逐步看一下这段代码。首先在顶层声明 liveADay
函数,然后立即调用它。它有一个 food
局部变量,还包含一个 eat
函数。然后调用 eat
功能。因为 eat
在 liveADay
内部,所以它“看到”了所有变量。这就是为什么它可以读取 food
变量的原因。
这就是闭包。
我们说当函数(例如 eat
)读取或写入在其外部(例如在 food
中)声明的变量(例如 food
)时,存在闭包。
花一些时间多读几遍,并确保你已经理解了上面的代码代码。
下面是本文最开始介绍过的例子:
let users = ['Alice', 'Dan', 'Jessica'];
let query = 'A';
let user = users.filter(user => user.startsWith(query));
如果用函数表达式重写,则更容易注意到闭包:
let users = ['Alice', 'Dan', 'Jessica'];
// 1. 查询变量在外部声明
let query = 'A';
let user = users.filter(function(user) {
// 2. 我们处于嵌套函数中
// 3. 然后我们读取查询变量(在外部声明!)
return user.startsWith(query);
});
每当函数访问在其外部声明的变量时,我们就说它是一个闭包。这个术语本身在使用时有些宽松。在本例中,有些人把嵌套函数本身称为“闭包”。其他人可能会把访问外部变量的“技术”称为闭包。实际上这都没关系。
函数调用的幽灵
闭看似简单,但是这并不意味着他们没有自己的陷阱。如果你真正考虑一下,函数可以在外部读取和写入变量的事实将会产生深远的影响。这意味着只要可以调用嵌套函数,这些变量就会“存活”下去:
function liveADay() {
let food = 'cheese';
function eat() {
console.log(food + ' is good');
}
// Call eat after five seconds
setTimeout(eat, 5000);
}
liveADay();
在这里,food
是在 liveADay()
函数调用内的局部变量。在我们退出 liveADay
之后,很容易想到它“消失了”,并且它不会回来困扰我们。
但是,在 liveADay
内部,我们告诉浏览器在五秒钟内调用 eat
。然后,eat
读取 food
变量。 因此,JavaScript引擎需要使特定的 liveADay()
调用中的food
变量保持可用,直到调用eat
。
从这种意义上讲,我们可以将闭包视为过去函数调用的“幻象”或“内存”。即使我们的 liveADay()
函数调用已经完成很长时间,但只要仍可以调用嵌套的 eat
函数,那么它的变量就必须继续存在。幸运的是,JavaScript 为我们做到了这一点,因此我们就无需再去考虑它了。
为什么会有“闭包”?
最后,你可能想知道为什么以这种方式调用闭包。主要是历史原因。一位熟悉计算机科学术语的人可能会说像 user => user.startsWith(query)
之类的表达式具有“开放绑定”。换句话说,从中可以清楚地知道 user
是什么(一个参数),但是还不能确定 query
是孤立的。当我们说“实际上,query
指的是在外部声明的变量”时,我们是在“关闭”开放绑定。换句话说,我们得到一个 闭包。
并非所有语言都实现闭包。例如在一些像 C 这样的语言中,根本不允许嵌套函数。结果,一个函数只能访问自己的局部变量或全局变量,永远不会出现访问父函数的局部变量的情况。当然,这种限制是痛苦的。
还有像 Rust 这样的语言,它们实现了闭包,但是对于闭包和常规函数有着单独的语法。因此,如果你想从函数外部读取变量,则必须在 Rust 中选择使用该变量。这是因为在底层,即使在函数调用之后,闭包也可能要求引擎保持外部变量(称为“环境”)。这种开销在 JavaScript 中是可以接受的,但是对于非常低级的语言来说,则可能会引发性能方面的问题。
到此为止,希望你能对闭包的概念有了深入理解!
本文首发微信公众号:前端先锋
欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章
欢迎继续阅读本专栏其它高赞文章:
- 深入理解Shadow DOM v1
- 一步步教你用 WebVR 实现虚拟现实游戏
- 13个帮你提高开发效率的现代CSS框架
- 快速上手BootstrapVue
- JavaScript引擎是如何工作的?从调用栈到Promise你需要知道的一切
- WebSocket实战:在 Node 和 React 之间进行实时通信
- 关于 Git 的 20 个面试题
- 深入解析 Node.js 的 console.log
- Node.js 究竟是什么?
- 30分钟用Node.js构建一个API服务器
- Javascript的对象拷贝
- 程序员30岁前月薪达不到30K,该何去何从
- 14个最好的 JavaScript 数据可视化库
- 8 个给前端的顶级 VS Code 扩展插件
- Node.js 多线程完全指南
- 把HTML转成PDF的4个方案及实现
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。