关于javascript:探索闭包

43次阅读

共计 4075 个字符,预计需要花费 11 分钟才能阅读完成。

翻译:疯狂的技术宅

原文: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 性能。因为 eatliveADay 外部,所以它“看到”了所有变量。这就是为什么它能够读取 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 个计划及实现

  • 更多文章 …

正文完
 0