[译]理解JS中的闭包

栏目: JavaScript · 发布时间: 5年前

内容简介:此篇文章翻译自Sukhjinder Arora文章闭包是每一个js开发者都需要知道和理解的概念。然而,它也是一个困扰着所有小萌新的概念。如果对于闭包有正确理解的话,他会帮助你写出更好更快更强的代码。那也就是说,它会帮助你成为一个更好js开发者。

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

理解JS中的闭包

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

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

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

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

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

执行上下文

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

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

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

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

[译]理解JS中的闭包
执行上下文例子

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

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

[译]理解JS中的闭包
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';
    
    return function DisplayName(){
        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: <getCountLexicalEnvironment>
}
复制代码

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开发人员。

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


以上所述就是小编给大家介绍的《[译]理解JS中的闭包》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

算法设计、分析与实现

算法设计、分析与实现

徐子珊 / 2012-10 / 65.00元

《算法设计、分析与实现:c、c++和java》由徐子珊编著,第1章~第6章按算法设计技巧分成渐增型算法、分治算法、动态规划算法、贪婪算法、回溯算法和图的搜索算法。每章针对一些经典问题给出解决问题的算法,并分析算法的时间复杂度。这样对于初学者来说,按照算法的设计方法划分,算法思想的阐述比较集中,有利于快速入门理解算法的精髓所在。一旦具备了算法设计的基本方法,按应用领域划分专题深入学习,读者可以结合已......一起来看看 《算法设计、分析与实现》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具