Dan Abramov谈JavaScript闭包

对于程序员来说,闭包是困难的,因为它们是“看不见的”构造。



当使用对象,变量或函数时,您是故意这样做的。您认为,“这是我需要一个变量的地方”,然后将其添加到代码中。 但是,关闭是另外一回事。虽然大多数程序员开始学习闭包,但这些人已经在使用闭包而不了解它。可能同一件事发生在您身上。因此,学习闭包与其说是学习一个新概念,不如说是学习如何识别您以前遇到过的很多事情。 简而言之,闭包是指函数访问在其外部声明的变量。例如,闭包包含在这段代码中:















let users = ['Alice', 'Dan', 'Jessica'];
let query = 'A';
let user = users.filter(user => user.startsWith(query));


请注意,这user => user.startsWith(query)是一个功能。她使用一个变量query并且此变量在函数外部声明。这是一个关闭。



如果愿意,可以跳过阅读。该材料的其余部分以不同的眼光看待封闭件。本文的这一部分将不讨论闭包是什么,而将详细介绍检测闭包。这类似于1960年代第一批程序员的工作方式。



步骤1:函数可以访问在其外部声明的变量



要了解闭包,您需要相当熟悉变量和函数。在此示例中,我们food在函数内部声明了一个变量eat



function eat() {
  let food = 'cheese';
  console.log(food + ' is good');
}
eat(); //    'cheese is good'


如果您希望以后可以food在函数外部更改变量的值eat怎么办?为此,我们可以从函数中删除变量本身,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();


阅读前面两个代码示例,并确保它们等效。



第二个示例有效!但是,让我们仔细看一下。请注意,该函数在functioneat内部liveADayJavaScript允许这样做吗?真的可以将一个函数包装在另一个函数中吗?



在某些语言中,以这种方式构造的代码将不正确。例如,在C语言中,这样的代码将是错误的(此语言没有闭包)。这意味着在使用C时,我们的第二个结论将是不正确的-您不能只接受任意一段代码并将其“包装”在函数中。但是JavaScript中没有这样的限制。



让我们再次考虑这段代码,特别注意变量的声明位置和使用位置。food



function liveADay() {
  let food = 'cheese'; //  `food`
  function eat() {
    console.log(food + ' is good'); //   `food`
  }
  eat();
}
liveADay();


让我们一起逐步研究这段代码。首先,我们在顶层声明一个函数liveADay。我们马上打电话给她。这个函数有一个局部变量food。该函数也在其中声明eat。然后在内部liveADay调用函数eat。由于该函数eat位于函数内部liveADay,因此它eat“看到”在中声明的所有变量liveADay。这就是函数eat可以读取变量值的原因food



这称为关闭。



我们谈论当函数(例如eat)读取或写入在food其外部声明的变量(例如的值(例如,在函数中liveADay时闭包的存在



考虑一下这些单词,重新阅读它们。通过查找示例代码中的内容来测试自己。



这是本文开头给出的示例:



let users = ['Alice', 'Dan', 'Jessica'];
let query = 'A';
let user = users.filter(user => user.startsWith(query));


通过使用函数表达式重写此示例可能更容易注意到关闭:



let users = ['Alice', 'Dan', 'Jessica'];
// 1.  query    
let query = 'A';
let user = users.filter(function(user) {
  // 2.     
  // 3.      query (    !)
  return user.startsWith(query);
});


当函数访问在其外部声明的变量时,我们称其为闭包。该术语本身使用得很宽松。有人会称呼嵌套函数本身,如示例“ closure”所示。其他人可以通过将其称为“闭包”来引用外部变量访问器。实际上,这无关紧要。



函数调用鬼



对您而言,关闭似乎是一个看似简单的概念。但这并不意味着它们缺乏某些非显而易见的功能。如果您仔细考虑一个函数可以在其外部读取和写入变量值的事实,事实证明这会带来相当严重的后果。



例如,这意味着只要可以调用嵌套在另一个函数中的一个函数,这些变量就会“存在”。



function liveADay() {
  let food = 'cheese';
  function eat() {
    console.log(food + ' is good');
  }
  //  eat   
  setTimeout(eat, 5000);
}
liveADay();


在此示例中,它food是函数call中的局部变量liveADay()。我只想确定此变量在退出函数后将“消失”,并且不会像幽灵一样再次困扰我们。



但是在函数中,liveADay我们要求浏览器eat在五秒钟后调用该函数。并且此函数读取变量的值food。结果,事实证明JavaScript引擎需要保持food与此调用关联的变量有效liveADay()直到调用该函数为止eat



从这种意义上讲,闭包可以被视为过去函数调用的“鬼魂”,或者被视为此类调用的“内存”。即使执行功能liveADay()在很早以前结束的情况下,只要eat可以调用嵌套函数,在其中声明的变量就必须继续存在幸运的是,JavaScript处理了这些机制,因此在这些情况下我们不需要做任何特殊的事情。



为什么“关闭”这样称呼?



您可能想知道为什么以这种方式调用“关闭”。其原因主要是历史原因。任何熟悉计算机术语的人都可能会说这样的表达式user => user.startsWith(query)具有“开放绑定”。换句话说,从这个表达式中可以清楚地知道什么user(参数),但是当孤立地查看时,不清楚它是什么query。当我们说实际上query是在函数外部声明的变量时,我们正在“关闭”打开绑定。换句话说,我们得到一个关闭。



并非所有编程语言都实现闭包。例如,在某些语言(例如C)中,您根本不能使用嵌套函数。结果,该函数只能使用其局部变量或全局变量。但是,永远不会出现访问父函数的局部变量的情况。这实际上是一个非常不愉快的限制。



还有像Rust这样的语言实现了闭包。但是他们使用不同的语法来描述闭包和正常函数。结果,如果需要在函数外部读取变量的值,则需要使用使用Rust的特殊构造。这样做的原因是,即使在函数调用完成之后,使用闭包也可能需要语言的内部机制来存储外部变量(称为“环境”)。JavaScript可以接受系统上的这种额外负载,但是当以相当低级的语言使用时,它可能会导致性能问题。



现在,希望您能理解JavaScript中的闭包概念。



您在理解JavaScript概念时遇到困难吗?






All Articles