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

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

因此,在这篇文章中,我将会尝试解析闭包内部原理以及它在JavaScript中是如何工作的。

好,废话少说,让我们开始吧。

什么是闭包

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

在我们深入学习闭包之前,首先,我们先理解下词法作用域(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函数可以访问自己作用域下定义的变量已经全局作用域。

所以,上面代码的一个作用域链是这样的:

Global {
outer {
inner
}
}

注意到,inner函数被outer函数的词法作用域所包围,而outer函数又被全局作用域所包围。这就是inner函数可以访问outer函数以及全局作用域定义的变量的原因。

闭包的实际例子

在深入闭包是如何工作之前,我们先来看下闭包一些实际的例子。

// 例子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)对外部环境的引用。

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开发者。

如果你觉得这文章对你有帮助,请点个赞!

(完)

后记

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

原文

原文链接

【译】理解JavaScript闭包——新手指南的更多相关文章

  1. 我从来不理解JavaScript闭包,直到有人这样向我解释它...

    摘要: 理解JS闭包. 原文:我从来不理解JavaScript闭包,直到有人这样向我解释它... 作者:前端小智 Fundebug经授权转载,版权归原作者所有. 正如标题所述,JavaScript闭包 ...

  2. 深入理解JavaScript闭包【译】

    在<高级程序设计>中,对于闭包一直没有很好的解释,在stackoverflow上翻出了一篇很老的<JavaScript closure for dummies>(2016)~ ...

  3. 【转】深入理解JavaScript闭包闭包(closure) (closure)

    一.什么是闭包?"官方"的解释是:闭包是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分.相信很少有人能直接看懂这句话,因为他描述 ...

  4. 全面理解Javascript闭包和闭包的几种写法及用途

    好久没有写博客了,过了一个十一长假都变懒了,今天总算是恢复状态了.好了,进入正题,今天来说一说javascript里面的闭包吧!本篇博客主要讲一些实用的东西,主要将闭包的写法.用法和用途.  一.什么 ...

  5. 深入理解JavaScript闭包(closure)

    最近在网上查阅了不少javascript闭包(closure)相关的资料,写的大多是非常的学术和专业.对于初学者来说别说理解闭包了,就连文字叙述都很难看懂.撰写此文的目的就是用最通俗的文字揭开Java ...

  6. 深入理解javascript闭包(一)

    闭包(closure)是Javascript语言的一个难点.也是它的特色,非常多高级应用都要依靠闭包实现. 一.什么是闭包? 官方"的解释是:闭包是一个拥有很多变量和绑定了这些变量的环境的表 ...

  7. 深入理解javascript闭包(一)

    原文转自脚本之家(http://www.jb51.net/article/24101.htm) 闭包(closure)是Javascript语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现. ...

  8. 深入理解Javascript闭包概念

    一.变量的作用域 要理解闭包,首先必须理解Javascript特殊的变量作用域. 变量的作用域无非就是两种:全局变量和局部变量. Javascript语言的特殊之处,就在于函数内部能够直接读取全局变量 ...

  9. 轻松理解JavaScript闭包

    摘要 闭包机制是JavaScript的重点和难点,本文希望能帮助大家轻松的学习闭包 一.什么是闭包? 闭包就是可以访问另一个函数作用域中变量的函数. 下面列举出常见的闭包实现方式,以例子讲解闭包概念 ...

随机推荐

  1. 左侧滚动条js

    <script> var left = document.getElementById('main-left'); var right = document.getElementById( ...

  2. Redis高级特性介绍及实例分析

    转自:http://www.jianshu.com/p/af7043e6c8f9   Redis基础类型回顾 String Redis中最基本,也是最简单的数据类型.注意,VALUE既可以是简单的St ...

  3. 我的大学,我的SPR机器人队

    时间过的真快,我这个在协会呆了好多年的老油条今年都毕业了,在石油大学大学七年几乎三分之二的时间就是在协会度过的.实话说在北京这是我最亲切的地方,这里有我喜欢的各种设备,有亲爱的老师和一起奋斗的队友,在 ...

  4. VC++ 错误

    1.error LNK2019: unresolved external symbol _WinMain@16 referenced in function ___tmainCRTStartup解决方 ...

  5. Light OJ 1011

    题意: (好难看) 给你 N 个 男的, 女的, 男的选女票, 题目给出矩阵, Mp[i][j] 表示 第 i 个男的选 第 J 个女的优先值 选了 J 之后的就不能选 J 了: 求所有狗男女的最大优 ...

  6. ICO和IPO

    ICO 和 IPOIPO:在经过种子轮,天使轮,x轮之后已经非常成熟的状态下进行的投资,基本上是盈利的,上市的是股份,现金流ICO:早期有一个idea,然后又很多的小散户进行的几千万-几亿的投资,风险 ...

  7. SVG前戏—让你的View多姿多彩

    什么是SVG SVG的全称是Scalable Vector Graphics,叫可缩放矢量图形.是一种基于可扩展标记语言(XML).它和位图(Bitmap)相对,SVG不会像位图一样因为缩放而让图片质 ...

  8. Windows Service 2012 R2 下如何建立ftp服务器

    1.首先在本地机器上创建一个用户!这些用户是用来登录到FTP的!我的电脑右键->管理->本地用户和组->用户->“右键”新建用户->输入用户名和密码再点创建就行了! 2. ...

  9. Go 开源博客平台 Pipe 1.0.0 发布!

    这是 Pipe 博客平台的第一个正式版,欢迎大家使用和反馈建议! 简介 Pipe 是一款小而美的开源博客平台,通过黑客派账号登录即可使用. 动机 产品层面: 市面上缺乏支持多独立博客的平台级系统 实现 ...

  10. Confluence 6 站点高级自定义

    你可以继续编辑的全局布局文件来继续更新你的主面板.请查看 Customizing the Confluence Dashboard 页面来获得更多有关的信息.你需要具有一些基本的Velocity 知识 ...