【译】理解JavaScript闭包——新手指南

闭包是JavaScript中一个基本的概念,每一个JavaScript开发者都应该知道和理解的。然而,不少新手JavaScript开发者对这个概念仍是很困惑的。javascript

正确理解闭包能够帮助你写出更好、更高效、简洁的代码。同时,这将会帮助你成为更好的JavaScript开发者。java

所以,在这篇文章中,我将会尝试解析闭包内部原理以及它在JavaScript中是如何工做的。数据结构

好,废话少说,让咱们开始吧。闭包

什么是闭包

用一句话来讲就是,闭包是一个能够访问它外部函数做用域的一个函数,即便这个外部函数已经返回了。这意味着即便在函数执行完以后,闭包也能够记住及访问其外部函数的变量和参数。ide

在咱们深刻学习闭包以前,首先,咱们先理解下词法做用域(lexical scope)。函数

什么是词法做用域

JavaScript中的词法做用域(或者静态做用域)是指在源代码物理位置中变量、函数以及对象的可访问性。举个例子:学习

let a = 'global';
  function outer() {
    let b = 'outer';
    function inner() {
      let c = 'inner'
      console.log(c);   // prints 'inner'
      console.log(b);   // prints 'outer'
      console.log(a);   // prints 'global'
    }
    console.log(a);     // prints 'global'
    console.log(b);     // prints 'outer'
    inner();
  }
outer();
console.log(a);         // prints 'global'

这里的inner函数能够访问本身做用域下定义的变量和outer函数的做用域以及全局做用域。而outer函数能够访问本身做用域下定义的变量已经全局做用域。
因此,上面代码的一个做用域链是这样的:ui

Global {
  outer {
    inner
  }
}

注意到,inner函数被outer函数的词法做用域所包围,而outer函数又被全局做用域所包围。这就是inner函数能够访问outer函数以及全局做用域定义的变量的缘由。线程

闭包的实际例子

在深刻闭包是如何工做以前,咱们先来看下闭包一些实际的例子。code

// 例子1
function person() {
  let name = 'Peter';
  
  return function displayName() {
    console.log(name);
  };
}
let peter = person();
peter(); // prints 'Peter'

在这段代码中,咱们调用了返回内部函数displayName的person函数,并将该函数存储在perter变量中。当咱们调用perter函数时(其实是引用displayName函数),名字“Perter”会打印到控制台。
可是在displayName函数中并无定义任何名为name到变量,因此即便该函数返回了,该函数也能够用某种方式访问其外部函数person的变量。因此displayName函数其实是一个闭包。

// 例子2
function getCounter() {
  let counter = 0;
  return function() {
    return counter++;
  }
}
let count = getCounter();
console.log(count());  // 0
console.log(count());  // 1
console.log(count());  // 2

一样地,咱们经过调用getCounter函数返回一个匿名内部函数,而且保存到count变量中。因为count函数如今是一个闭包,能够在即便在getCounter函数返回后访问getCounter函数的变量couneter。
可是请注意,counter的值在每次count函数调用时都不会像一般那样重置为0。
这是由于,在每次调用count()的时候,都会建立新的函数做用域,可是只为getCounter函数建立一个做用域,由于变量counter定义在getCounter函数做用域内,因此每次调用count函数时数值会增长而不是重置为0。

闭包工做原理

到目前为止,咱们已经讨论了什么是闭包以及一些实际的例子。下面咱们来了解下闭包在javaScript中的工做原理。
要真正理解闭包在JavaScript中的工做原理,首先,咱们必需要理解JavaScript中的两个重要的概念:1)执行上下文 2)词法环境。

执行上下文(Execution Context)

执行上下文是一个抽象的环境,其中的JavaScript代码会被计算求值和执行。当全局代码执行时,它在全局执行上下文中执行,函数代码在函数执行上下文中执行。

当前只能有一个正在运行执行环境(由于JavaScript是单线程语言),它由被称为执行堆栈或调用堆栈的堆栈数据结构管理。

执行堆栈是一个具备LIFO(后进先出)结构的堆栈,其中只能在堆栈顶部进行添加或删除选项。

当前正在运行的执行上下文始终位于堆栈的顶部,当正在执行的函数执行完成后,其执行上下文将从堆栈中弹出移除,而后控制到达堆栈中它下面的执行上下文。

下面咱们看一个代码片断更好地理解执行上下文和堆栈。

当以上代码执行时,JavaScript引擎会建立一个全局执行上下文来执行全局代码,而后当执行到调用first()函数时,它会为该函数建立一个新的执行上下文而且将其推送到执行堆栈的顶部。
因此,上面代码的执行堆栈就以下图那样:

当first()函数执行完后,它的执行堆栈就会从堆栈中移除。而后,控制到达下一个执行上下文,就是全局执行上下文了。所以,将会执行全局做用域下剩余的代码。

词法环境(Lexical Envirionment)

每次JavaScript引擎建立一个执行上下文执行函数或者全局代码时,它还会建立一个新的词法环境来存储在该函数执行期间在该函数中定义的变量。

词法环境是一个包含标识符(identifier)-变量(variable)映射的数据结构。(这里所说的标识符(identifier)指的是变量或者函数的名称,而变量(variable)是实际对象[包括函数类型对象]或原始值的引用)。

一个词法环境有两个组件:(1)环境数据 (2)对外部环境的引用。

一、环境数据是指变量和函数声明实际存放的地方。

二、对外部环境的引用意思是说它能够访问外部(父级)的词法环境。这个组件很重要,是理解闭包工做原理的关键。

一个词法环境从概念上看起来像这样:

lexicalEnvironment = {
  environmentRecord: {
    <identifier> : <value>,
    <identifier> : <value>
  }
  outer: < Reference to the parent lexical environment> // 父级词法环境引用
}

如今咱们来从新看下以前上面的代码片断:

let a = 'Hello World!';
function first() {
  let b = 25;  
  console.log('Inside first function');
}
first();
console.log('Inside global execution context');

当JavaScript引擎建立一个全局执行上下文来执行全局代码时,它还建立了一个新的词法环境来存储在全局做用域定义的变量和函数。所以,全局做用域的词法环境将以下所示:

globalLexicalEnvironment = {
  environmentRecord: {
      a     : 'Hello World!',
      first : < reference to function object >
  }
  outer: null
}

这里的外部词法环境设置为null,由于全局做用域没有外部词法环境。
当引擎为first()函数建立执行上下文时,它还会建立一个词法环境来存储在执行函数期间在该函数中定义的变量。 因此函数的词汇环境看起来像这样:

functionLexicalEnvironment = {
  environmentRecord: {
      b    : 25,
  }
  outer: <globalLexicalEnvironment>
}

函数的外部词法环境设置为全局词法环境,由于该函数被源代码中的全局做用域所包围。

详细的闭包示例

如今咱们理解了执行上下文和词法环境了,下面咱们回到闭包。

例子一

咱们先看下这个代码块

function person() {
  let name = 'Peter';
  
  return function displayName() {
    console.log(name);
  };
}
let peter = person();
peter(); // prints 'Peter'

当person函数执行,JavaScript引擎会给这个函数建立一个新的执行上下文和词法环境。当该函数执行完成后,将返回displayName函数而且分配给到perter变量。
因此它的词法环境看起来像这样:

personLexicalEnvironment = {
  environmentRecord: {
    name : 'Peter',
    displayName: < displayName function reference>
  }
  outer: <globalLexicalEnvironment>
}

当person函数执行完成后,它的执行上下文就会从堆栈里移除。但它的词法环境仍然在内存里,是由于它的词法环境被它内部的displayName函数的词法环境引用。因此变量在内存中仍然可用。

当peter函数执行(实际上是引用displayName函数),JavaScript引擎会为该函数建立新的执行上下文和词法环境。
因此它的词法环境看起来像这样:

displayNameLexicalEnvironment = {
  environmentRecord: {
    
  }
  outer: <personLexicalEnvironment>
}

由于displayName函数没有声明变量,因此它的环境数据是空的。该函数在执行期间,javaScript引擎将尝试在该函数的词法环境中寻找变量name。
由于displayName函数的词法环境没有任何变量,因此引擎会到外层的词法环境寻找,这就是还在内存中的person函数的词法环境。JavaScript引擎找到了这个变量name而后打印到控制台。

例子二

function getCounter() {
  let counter = 0;
  return function() {
    return counter++;
  }
}
let count = getCounter();
console.log(count());  // 0
console.log(count());  // 1
console.log(count());  // 2

一样地,getCounter函数的词法环境是这样的:

getCounterLexicalEnvironment = {
  environmentRecord: {
    counter: 0,
    <anonymous function> : < reference to function>
  }
  outer: <globalLexicalEnvironment>
}

这个函数返回一个匿名函数而且把它分配到变量count。
当这个count函数执行,它的词法环境看起来是这样的:

countLexicalEnvironment = {
  environmentRecord: {
  
  }
  outer: <getCountLexicalEnvironment>
}

当count函数被调用,Javascript引擎会尝试在该函数词法环境查找变量counter。一样地,由于它的环境数据是空的,因此引擎将到该函数外层词法环境查找。
所以,在第一次调用count函数以后getCounter函数的词法环境是这样的:

getCounterLexicalEnvironment = {
  environmentRecord: {
    counter: 1,
    <anonymous function> : < reference to function>
  }
  outer: <globalLexicalEnvironment>
}

在每次调用count函数,Javascript引擎都会为count函数建立一个新的词法环境,递增count变量而且更新getCounter函数的词法环境以表示作了变动。

结语

因此咱们学习了什么是闭包和闭包的原理。闭包是JavaScript的基本概念,每一个JavaScript开发者都应该理解的。熟悉这些概念将有助于你成为一个更高效、更好的JavaScript开发者。
若是你以为这文章对你有帮助,请点个赞!
(完)

后记

以上译文仅用于学习交流,水平有限,不免有错误之处,敬请指正。

原文

原文连接

相关文章
相关标签/搜索