Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

理解 JavaScript 中的闭包 #11

Open
RicoLiu opened this issue Oct 9, 2018 · 0 comments
Open

理解 JavaScript 中的闭包 #11

RicoLiu opened this issue Oct 9, 2018 · 0 comments

Comments

@RicoLiu
Copy link
Owner

RicoLiu commented Oct 9, 2018

理解 JavaScript 中的闭包

学习 JavaScript 中的闭包是如何工作的

英文原文

https://blog.bitsrc.io/a-beginners-guide-to-closures-in-javascript-97d372284dda

0_n8doeigwudctv6jm

闭包是 JavaScript 开发者应该知道和了解的基本概念。然而,它还是会使许多新手开发者感到困惑。

正确的理解闭包能够帮助你编写更好、更高效、更整洁的代码。相反地,它将会帮助你成为更好的 JavaScript 开发者。

所以,在这篇文章中,我将会深层的解析闭包以及它是如何在 JavaScript 中运行的。

现在,让我们开始吧。:)

什么是闭包

闭包是能够访问到它外层函数作用域的一个函数,甚至在外层函数已经返回之后也是可以的。这就意味着:闭包能够记录和访问到它外层函数的变量和参数,即使在外层函数已经运行结束。

在我们深入了解闭包之前,让我们先理解词法作用域。

什么是词法作用域

在 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 函数能够访问到定义在它内部的变量以及全局作用域。

所以,上段代码的作用域链如下:

Global {
  outer {
    inner
  }
}

注意到:inner 函数被 outer 函数的词法作用域所包围,同样地,outer 函数被全局作用域所包围。这就是为什么 inner 函数能够访问到 outer 函数定义的变量以及全局作用域。

闭包的练习

让我们在深入闭包如何运行之前看一些例子。

Example 1

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

在这段代码中,我们调用 person 函数,其返回了内部的 display 函数并且赋值给 peter 变量。当我们调用 peter 函数时(实际是 display 函数的引用),'Peter' 将会打印在控制台上。

但是在 display 函数内部,并没有任何变量名为 name 的变量,所以该函数能够访问到它外层函数的变量,即使该函数已经被返回。所以, display 函数实际上就是一个闭包。

Example 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 函数的 counter 变量,即使在 getCounter() 返回之后。

但是注意到,在每一次 count 函数被调用时,counter 的值没有像通常那样被重置为 0

这是因为,在每一次调用 count() 时,就会创建一个新的作用域,但是 getCounter 函数只创建了一个作用域。因为 counter 变量是定义在 getCounter 函数作用域内部的,它将会在每一次调用 count 函数时进行自增,而不是重置为 0.

闭包是如何运行的

到目前为止,我们已经讨论了什么是闭包以及闭包的例子。现在,让我们理解在 JavaScript 中闭包是如何运行的。

为了真正的理解 JavaScript 中闭包的工作机制,我们不得不理解两个最重要的概念。1)执行上下文、2)词法环境。

执行上下文

执行上下文是一个 JavsScript 代码解析和运行的抽象环境。当全局代码被执行时,它是运行在全局的执行上下文中,并且函数代码是在函数内部执行上下文中运行的。

目前只能有一个正在运行的执行上下文(因为 JavaScript 是单线程语言),执行上下文是通过栈的数据结构来进行管理的,我们称之为执行栈或调用栈。

执行栈本质上就是一个栈,其拥有着 LIFO 的数据结构(后进先出),栈中的元素只能通过栈顶来添加和删除。

当前运行的执行上下文永远是在栈顶,当函数完全运行完,它的执行上下文将会从栈顶移除,并且控制达到它在堆栈中下一个的执行上下文。

来看一个代码片段以便更好的理解执行上下文和堆栈:

default

当代码执行时,JavaScript 引擎创建了一个全局的执行上下文来执行全局的代码,当代码执行到 first() 函数时,它会为该函数创建一个新的执行上下文并且将其压栈到执行堆栈当中。

所以,上段代码的执行堆栈如下所示:

default

first() 函数完成时,它的执行上下文从堆栈中移除,并且控制其下一个的执行堆栈---全局执行上下文。所以在全局作用域中剩余的代码将会执行。

词法环境

每次 JavaScript 引擎创建一个执行上下文来执行函数或全局代码,它也会在函数运行时创建一个新的词法环境来存储定义在函数内部的变量。

词法环境是拥有 identifier-variable mapping 的数据结构。这里的, identifier 指的是变量和函数的名称,variable 指的是实际的对象(包含函数类型对象)或原始值。

词法环境拥有两个组件:(1)the environment record、(2)a reference to the outer environment.

  1. environment record 是一个存储变量和函数声明的地方。

  2. reference to the outer environment 意味着它能够访问到它父级的词法环境。这个组件对于理解闭包的运行原理有着非常重要的作用。

词法环境的概念如下所示:

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>
}

该函数的外层词法环境被设置为全局的词法环境,是因为在源代码中,该函数被全局作用域所包裹。

注意 --- 当函数执行完,它的执行上下文从执行堆栈中移除,但是它的词法环境可能会被移除、也可能不会被移除,这取决于它的词法环境是否被其外层的词法环境中的属性里的任何其他词法环境所引用。

详细的闭包例子

现在我们理解了执行上下文以及词法环境,让我们回到闭包。

Example 1

看看下面的代码片段:

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

当运行 person 方法时, JavaScript 引擎为该方法创建了一个新的执行上下文和词法环境,在该方法运行结束后,他将 displayName 函数返回并将其赋值给 peter 变量。

所以,它的词法环境如下所以:

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

person 函数运行完时,它的执行上下文会同时从执行堆栈中移除。但是它的词法环境依旧在内存中,这是因为,它的词法环境被它内部的 displayName 函数的词法环境引用了,所以,它的词法环境在内存中依旧可用。

peter 函数运行时(实际上就是 displayName 函数的引用),JavaScript 引擎为该函数创建了一个新的执行上下文和词法环境。

所以,它的词法环境如下所示:

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

因为 displayName 函数中没有变量,所以它的 environment record 为空。在该方法执行中,JavaScript 引擎将会在函数的词法环境中寻找变量 name.

因为 displayName 函数中的词法环境内没有变量,所以它将会寻找它外层的词法环境,也就是 person 函数的词法环境依旧在内存当中。JavaScript 引擎找到了变量并将 name 打印在控制台上。

Example 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 函数的词法环境如下所示:

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

该函数返回一个匿名函数并将其赋值给 count 变量。

count 函数执行时,它的词法环境如下所示:

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

count 函数被调用,JavaScript 引擎将会找寻该函数的词法环境内的 counter 变量。因为它的 environment record 为空,所以 JavaScript 引擎将会找寻其外层函数的词法环境。

JavaScript 引擎找到了变量,将其打印在控制台并会增加 getCounter 函数词法环境内的 counter 变量。

所以,在第一次调用 getCounter 函数之后,其词法环境如下所示:

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

每一次调用 count 函数,JavaScript 引擎都会为 count 函数创建一个新的词法环境,增加 counter 变量值并且更新 getCounter 函数的词法环境来反映变化。

结论

目前,我们已经学习了闭包是如何运行工作的。闭包是 JavaScript 中基本概念,每一个 JavaScript 开发者都应该理解它。熟练掌握这些概念有助于你成为更好、更高效的 JavaScript 开发者。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant