理解JS中的闭包
写在正文前
此篇文章翻译自文章. 这篇文章结合了闭包,词法作用域,调用栈以及执行上下文来理解闭包。文章如有翻译不好的地方还望多多包涵。
理解JS中的闭包
闭包是每一个js开发者都需要知道和理解的概念。然而,它也是一个困扰着所有小萌新的概念。
如果对于闭包有正确理解的话,他会帮助你写出更好更快更强的代码。那也就是说,它会帮助你成为一个更好js开发者。
因此在这个文章内,我将会尝试解释闭包的内部原理,以及他们是如何在实际js中运行的。
屁话不多说,我们开始吧:)
(广告时间) 小贴士:当写了可复用的js代码的时候,你可能想要不仅仅在一个项目中使用他们. 是一个非常有用对对小工具方便你快速的分享和整理你的可复用代码,
执行上下文
执行上下文是js代码赋值和执行的抽象环境。当全局代码执行的时候,他就执行在全局执行上下文内部。函数代码执行在函数执行上下文内部。
js中有且只有一个当前正在运行的执行上下文(因为js是单线程语言),这个执行上下文是有一个栈来控制的,通常被称为执行栈或者调用栈。
执行栈是有LIFO(后进先出)特点的栈结构,事物只能从栈顶添加或者移出。 当前运行的执行上下文总是栈的最顶部,并且当当前运行的函数结束的时候,他的执行上下文会从栈顶弹出然后控制器到栈中的下一个执行上下文。
让我们看一个小的代码片段来更好的理解执行上下文和栈:
当代码执行的时候,js引擎会创建一个全局的执行上下文来执行全局的代码,当它碰到了对first()
函数的调用,他为函数创建了一个新的执行上下文并把它推入执行栈的栈顶。
所以上述代码的执行栈如下图所示
当first()
函数结束的时候,他的执行上下文从执行栈移出,控制器到达他下面的执行上下文也就是全局执行上下文。所以全局作用域中剩下的代码将会被继续执行。
词法环境
每次JavaScript引擎创建一个执行上下文来执行函数或者全局代码, 它同时也会创建一个新的词法环境来存储在函数执行过程中定义在函数内部的变量。
词法环境是一个保存标识符-变量的映射的数据结构。(此处标识符指的是变量或者函数的名字,变量是对实际对象[包括函数类型对象]或原始值的引用)
一个词法环境要有两部分组成:(1)环境记录 以及 (2)一个对外部环境的引用
- 环境记录是变量和函数声明真实的存储位置
- 对外部环境的引用以为着它可以访问其外部词法环境。这部分是理解闭包怎么工作的最重要的部分。
一个词法环境理论上应该长成这个样子:
lexicalEnvironment = { environmentRecord: {: , : , <标识符> : <值> }, 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 :}, outer: null}复制代码
在这里外部的词法环境被设置为null因为没有比全局作用域更外部的词法环境。
当引擎创建first
函数的执行上下文的同时,它也为函数创建了一个词法环境来存储在执行函数的过程中定义在函数内部的变量。因此函数的词法环境应该是这个样子:
functionLexicalEnvironment:{ environmentRecord: { b : 25 }, outer:}复制代码
函数的外部词法环境被设置为全局词法环境,因为函数在源码中被全局作用域包含着。
注意- 当一个函数结束调用的时候,他的执行上下文被从栈顶移出,但是他的词法环境可能也可能不从内存中移出 ,这取决于词法环境实发被其他词法环境在他们的外部词法环境引用。
一个更详细的闭包例子:
现在我们理解了执行上下文和词法环境,让我们回到闭包。
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:}复制代码
当peter
函数执行的时候(实际上是对displayName
函数的引用),js引擎为函数创建了一个新的执行上下文和词法环境。
因此它的词法环境长成这个样子:
displayNameLexicalEnvironment = { environmentRecord: { } outer:}复制代码
因为在displayName
函数内部没有私有变量,因此它的环境记录是空的。在执行函数的过程中,js引擎尝试在他的词法环境中寻找变量name
。 因为在displayName
函数的词法作用域中没有变量,所以引擎会在他的外部词法环境寻找这个变量,也就是说,person
函数的词法环境还是在内存中的。JS引擎找到了变量,并把name
在控制台输出。
Example3
function getCounter(){ let counter = 0; return function(){ return counter++; }}let count = getCounter();console.log(count());//0console.log(count());//1console.log(count());//2复制代码
再来一遍,getCounter
函数的词法环境应该长成这个样子:
getCounterLexicalEnvironment = { environmentRecord: { counter: 0,: }, outer: }复制代码
这个函数返回了一个匿名函数并把它赋值给了count
变量。
当count
函数被执行的时候,他的词法作用域是这个样子的:
countLexicalEnvironment = { environmentRecord: { }, outer:}复制代码
当count
函数调用的时候,JS引擎在该函数的词法作用域里面寻找了一下counter
变量。他的环境记录也是空的,引擎便会去他的外层词法环境去找。
引擎找到了变量,把它输出到控制台,然后在getCounter
函数的词法作用域中增加了counter变量的值。
所以getCounter
函数的词法作用域在第一次调用count之后变成了这个样子
getCounterLexicalEnvironment = { environmentRecord: { counter: 1,: }, outer: }复制代码
在每次的count
函数调用之后,js创建了一个新的count
的词法作用域,递增了counter
变量然后更新了getCounter
函数的词法作用域来反应变化。
结论
所以我们已经了解了什么是闭包以及它们是如何工作的。 闭包是每个JavaScript开发人员都应该理解的JavaScript的基本概念。 熟悉这些概念将有助于您成为一个更有效,更好的JavaScript开发人员。
就是这样,如果你发现这篇文章有用,请点击下面的拍手?按钮,你也可以在 社交媒体和Twitter上关注我,如果你有任何疑问,请随时发表评论! 我很乐意帮忙:)
译者注
新的一年,还是要努力的提升自己:)祝大家新春快乐