理解 JS 中的闭包

写在正文前

此篇文章翻译自Sukhjinder Arora文章Understanding Closures in JavaScript. 这篇文章结合了闭包,词法作用域,调用栈以及执行上下文来理解闭包。文章如有翻译不好的地方还望多多包涵。

理解 JS 中的闭包

闭包是每一个 js 开发者都需要知道和理解的概念。然而,它也是一个困扰着所有小萌新的概念。

如果对于闭包有正确理解的话,他会帮助你写出更好更快更强的代码。那也就是说,它会帮助你成为一个更好 js 开发者。

因此在这个文章内,我将会尝试解释闭包的内部原理,以及他们是如何在实际 js 中运行的。

屁话不多说,我们开始吧:)

(广告时间) 小贴士:当写了可复用的 js 代码的时候, 你可能想要不仅仅在一个项目中使用他们. Bit是一个非常有用对对小工具方便你快速的分享和整理你的可复用代码,

执行上下文

执行上下文是 js 代码赋值和执行的抽象环境。当全局代码执行的时候,他就执行在全局执行上下文内部。函数代码执行在函数执行上下文内部。

js 中有且只有一个当前正在运行的执行上下文(因为 js 是单线程语言),这个执行上下文是有一个栈来控制的,通常被称为执行栈或者调用栈。

执行栈是有 LIFO( 后进先出)特点的栈结构,事物只能从栈顶添加或者移出。 当前运行的执行上下文总是栈的最顶部,并且当当前运行的函数结束的时候,他的执行上下文会从栈顶弹出然后控制器到栈中的下一个执行上下文。

让我们看一个小的代码片段来更好的理解执行上下文和栈:

Execution Context Example

执行上下文例子

当代码执行的时候,js 引擎会创建一个全局的执行上下文来执行全局的代码,当它碰到了对first()函数的调用,他为函数创建了一个新的执行上下文并把它推入执行栈的栈顶。

所以上述代码的执行栈如下图所示

Execution Stack Execution Stack

first()函数结束的时候,他的执行上下文从执行栈移出,控制器到达他下面的执行上下文也就是全局执行上下文。所以全局作用域中剩下的代码将会被继续执行。

词法环境

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

词法环境是一个保存标识符 - 变量的映射的数据结构。(此处标识符指的是变量或者函数的名字,变量是对实际对象 [包括函数类型对象] 或原始值的引用)

一个词法环境要有两部分组成:(1)环境记录 以及 (2)一个对外部环境的引用

  1. 环境记录是变量和函数声明真实的存储位置
  2. 对外部环境的引用以为着它可以访问其外部词法环境。这部分是理解闭包怎么工作的最重要的部分。

一个词法环境理论上应该长成这个样子:

lexicalEnvironment = {
    environmentRecord: {
        <identifier> : <value>,
        <identifier> : <value>,
        <标识符> : <值>
    },
    outer: <Reference to the parent lexical environment>
    <!--outer:指向父词法环境-->
}
复制代码

所以让我们在看一遍上面的代码块:

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>
}
复制代码

函数的外部词法环境被设置为全局词法环境,因为函数在源码中被全局作用域包含着。

注意 - 当一个函数结束调用的时候,他的执行上下文被从栈顶移出,但是他的词法环境可能也可能不从内存中移出 ,这取决于词法环境实发被其他词法环境在他们的外部词法环境引用。

一个更详细的闭包例子:

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

Example1

让我们看一下下面的代码片段:

function Person(){
    let name = 'Peter';
<span class="hljs-built_in">return</span> <span class="hljs-keyword">function</span> <span class="hljs-function"><span class="hljs-title">DisplayName</span></span>(){
    console.log(name);
};

}

let peter = person();
peter();// 输出 ‘peter’

复制代码

person函数被执行的时候,JS 引擎为该函数创建了一个新的执行上下文和词法环境。在函数结束之后,他返回displayName函数并把它分配给peter 变量。

因此它的词法作用域长成这个样子:

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

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

因此它的词法环境长成这个样子:

displayNameLexicalEnvironment = {
  environmentRecord: {
  }
  outer: <personLexicalEnvironment>
}
复制代码

因为在displayName函数内部没有私有变量,因此它的环境记录是空的。在执行函数的过程中,js 引擎尝试在他的词法环境中寻找变量name。 因为在displayName函数的词法作用域中没有变量,所以引擎会在他的外部词法环境寻找这个变量,也就是说,person函数的词法环境还是在内存中的。JS 引擎找到了变量,并把name在控制台输出。

Example3

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: &lt;getCountLexicalEnvironment&gt;

}

复制代码

count函数调用的时候,JS 引擎在该函数的词法作用域里面寻找了一下counter变量。他的环境记录也是空的,引擎便会去他的外层词法环境去找。

引擎找到了变量,把它输出到控制台,然后在getCounter函数的词法作用域中增加了 counter 变量的值。

所以getCounter函数的词法作用域在第一次调用 count 之后变成了这个样子

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

在每次的count函数调用之后,js 创建了一个新的count的词法作用域,递增了counter变量然后更新了getCounter函数的词法作用域来反应变化。

结论

所以我们已经了解了什么是闭包以及它们是如何工作的。 闭包是每个 JavaScript 开发人员都应该理解的 JavaScript 的基本概念。 熟悉这些概念将有助于您成为一个更有效,更好的 JavaScript 开发人员。

就是这样,如果你发现这篇文章有用,请点击下面的拍手 * 按钮,你也可以在 社交媒体和 Twitter 上关注我,如果你有任何疑问,请随时发表评论! 我很乐意帮忙:)

译者注

新的一年,还是要努力的提升自己:)祝大家新春快乐

感谢    赞同    分享    收藏    关注    反对    举报    ...