原文地址: medium.com/dailyjs/i-n…
译文地址:github.com/xiao-T/note…
本文版权归原做者全部,翻译仅用于学习。javascript
正如标题所述,JavaScript 闭包对我来讲一直是一个谜。为此,我读过不少[文章]((en.wikipedia.org/wiki/Closur…),工做中我也用过闭包,有些时候,我甚至都不知道使用了闭包。java
最近,我和一些人讨论了一下,他们真正的点醒了我。在这篇文章中,我将会尝试解释一下闭包。首先,我要感谢一下 CodeSmith 和他们的JavaScript 的课程。git
为了理解闭包,有些概念很是重要。其中之一就是执行上下文。github
这篇文章很好的解释了什么是执行上下文。如下是引用:数组
当执行 JavaScript 代码时,执行环境很是重要,而且会按照如下状况计算:闭包
全局代码 — 当代码第一次执行时,默认的执行环境app
函数代码 — 在函数体内执行的代码函数
执行上下文
其实就是当前代码执行的环境/做用域。学习
换句话说,程序开始时,是在全局的执行上下文中。有些变量是在全局上下文中被声明定义的。咱们称之为全局变量。当程序在函数中执行,会发生什么呢?会有如下几步:ui
函数什么时候结束?当遇到 return
语句或者遇到闭合的大括号 }
。当函数结束时,会依次发生如下状况:
return
语句,默认,将会返回 undefined
。解释闭包以前,咱们先看一下如下的代码片断。它看起很是简单直接,任何人都知道会发生什么。
1: let a = 3
2: function addTwo(x) {
3: let ret = x + 2
4: return ret
5: }
6: let b = addTwo(a)
7: console.log(b)
复制代码
为了理解 JavaScript 引擎是如何工做的,咱们来逐步分析一下:
在第一行中,咱们在全局执行上下文中声明了一个变量 a
并赋值数字 3
。
接下来会变得棘手。第二行到第五行是一个总体。发生了什么呢?在全局执行上下文中咱们声明了一个名为 addTwo
的新变量。咱们给它分配什么呢?函数定义。两个大括号 { }
任何代码都会分配给 addTwo
。函数内部的代码,这时并不会被计算,也不会执行,只是存储在一个变量中以便未来使用。
如今,咱们来到了第六行。看起很是简单,可是,这里包含了不少东西。首先,在全局执行上下文中,咱们声明了一个新的变量 b
。变量声明的同时,也会赋值 undefined
。
接下来,仍然是第六行,咱们看到有一个赋值操做符。这时,咱们才真正赋值给变量 b
。接下来,咱们看到函数被调用了。当你看到一个变量后面跟着一个圆扣号 ()
,那表明着函数调用执行。如前所述,每一个函数都会返回一些值(值、对象或者 undefined
)。无论,函数返回什么都会赋值给变量 b
。
可是,首先咱们须要调用函数 addTwo
。JavaScript 将会在全局执行上下文中查找一个名为 addTwo
的变量。是的,它找到了,在第二步定义的(第二行到第五行)。你看,变量 addTwo
是一个函数。注意,变量 a
做为一个参数传递给了函数。JavaScript 会在全局执行上下文中搜索变量 a
并找到它,发现它的值是 3
,而后,数字 3
就传递给了函数。准备开始执行函数。
如今,执行上下文发生了改变。一个新的本地执行上下文被建立,咱们叫它 “addTwo 执行上下文”。这个执行上下文被压入到调用栈。在本地执行上下文中咱们首先要作什么呢?
你可能会说:“一个新的变量 ret
在本地执行上下文中被声明了”。这不对,正确的答案是,首先,咱们须要看一下函数的参数。在本地执行上下文中声明了一个新的变量 x
。而后,因为数字 3
作为参数传递给了函数,那么,变量 x
就的值就变成了 3
。
下一步:本地执行上下文声明了新的变量 ret
。它的值是 undefined
(第三行)
仍旧是第三行,须要执行加法。首先,咱们须要用到 x
的值。JavaScript 将会查找变量 x
。首先,它会在本地执行上下文中查找。并且,找到了,它的值是 3
。第二个操做数是数字 2
。二者相加以后的结果(5
)将会赋值给变量 ret
。
第四行。咱们会返回变量 ret
。另外,根据本地执行上下文的内容得知 ret
的值是 5
。函数将会返回数字 5
。这时,函数结束。
函数在第四到第五行结束。本地执行上下文也随之被销毁。变量 x
和 ret
同时被抹除。它们将会消失。调用栈也会弹出响应的上下文,返回值将会返回到调用上下文。在这个案例中,调用栈就是全局执行上下文,这是由于,函数 addTwo
是在全局执行上下文中被调用的。
如今,咱们从新回到第四步。返回值(数字5
)赋值给了变量 b
。咱们仍旧在程序的第六行。
我不用详解介绍,在第七行,变量 b
的值被输出到了控制台。它是数字 5
。
为了解释一个简单的程序费了很多口舌,然而,咱们尚未真正的讲道闭包。我保证我会的。首先,咱们须要绕个弯路。
咱们须要理解什么是词法做用域。看下面的代码。
1: let val1 = 2
2: function multiplyThis(n) {
3: let ret = n * val1
4: return ret
5: }
6: let multiplied = multiplyThis(6)
7: console.log('example of scope:', multiplied)
复制代码
想法是:咱们在本地执行上下文和全局执行上下文都有变量。JavaScript 中比较难理解是:如何查找变量。若是,在本地执行上下文中找不到,将会在自身的调用上下文中继续查找。若是,仍是没有找到。重复以上的动做,直到查到全局执行上下文。(若是,仍旧没有找到,就会返回 undefined
)。根据这个规则,上面的示例就很清晰了。若是,你清楚做用域是如何工做的,你能够跳过这部分。
在全局执行上下文中声明一个变量 val1
,而后,给它赋值数字 2
第 2 - 5 行。声明了一个新变量 multiplyThis
,而后,定义了一个函数
第 6 行。在全局执行上下文声明了一个变量 multiplied
在全局执行上下文中找到变量 multiplyThis
,并作为一个函数执行。而后,把数字 6
作为参数传递给函数
函数被调用 = 新的执行上下文。建立新的本地执行上下文
在本地执行上下文中,声明了变量 n
并赋值了数字 6
第 3 行。声明了变量 ret
第 3 行。变量 n
和 vall
两个数的相乘。在本地执行上下文中查找变量 n
。咱们在第 6 行声明了这个变量。它的值是数字 6
。本地上下文中没有找到变量 vall
。须要检测调用上下文。由于,调用上下文是全局上下文。咱们须要在全局上下文中查找 vall
。很好,找到了。它在第 1 行被定义的。它的值是数字 2
。
第 3 行。两个数相差,而后赋值给变量 ret
。6 * 2 = 12。ret
的值是 12
。
返回 ret
的值。随之本地上下文也被销毁,同时销毁的还有变量 ret
和 n
。变量 vall
并不会被销毁,由于它是全局上下文的一部分。
回到第 6 行。在调用上下文中,数字 12
被复制给变量 multiplied
。
最后的第 7 行,咱们在控制台中打印了变量 multiplied
的值
经过这个示例,咱们能够知道函数能够访问调用上下文中的变量。这种现象的正式名称就叫作词法做用域。
第一个示例中函数 addTwo
返回了一个数字。早前,咱们也说过函数能够返回任何类型。咱们来看一个函数返回函数的示例,这对于理解闭包相当重要。下面的演示,咱们会一点点分析它。
1: let val = 7
2: function createAdder() {
3: function addNumbers(a, b) {
4: let ret = a + b
5: return ret
6: }
7: return addNumbers
8: }
9: let adder = createAdder()
10: let sum = adder(val, 8)
11: console.log('example of function returning a function: ', sum)
复制代码
咱们开始一步步的分析下代码。
第 1 行。咱们在全局上下文中声明了变量 val
,而且赋值数字 7
给变量
第 2 - 8 行。在全局上下文中咱们声明了一个名为 createAdder
的函数。第 3 - 7 行就是函数的具体定义。和以前同样,这个时候,咱们并不会执行函数。咱们只是把函数赋值给一个变量(createAdder
)
第 9 行。在全局上下文中,咱们声明了新的变量 adder
。同时,它的值是 undefined
仍是第 9 行。咱们看到了一个圆括号()
;表明着咱们须要调用函数。咱们在全局上下文中搜索找了到名为 createAdder
的变量。它是在第 2 步建立的。好,咱们来调用它。
调用函数。如今,咱们回到第 2 行。一个新的上下文被建立。在新的上下文中咱们建立了本地变量。同时,引擎也会把新的上下文压入到调用栈。这个函数没有参数,咱们直接看它的内部。
第 3 - 6 行。咱们又声明了一个新的函数。在本地上下文中咱们建立变量 addNumber
。这个很重要。addNumber
只在本地上下文中有效。在本地上下文中咱们定义了一个函数并命名为 addNumber
如今,我来到第 7 行。咱们返回了变量 addNumber
。引擎会查找变量 addNumber
,固然也会找到它。它是一个函数。好,函数能够返回任何东西,包括函数。所以,咱们返回了 addNumbers
的函数体。在第 4 - 5 行就是函数的具体定义。同时,咱们也把本地上下文从调用栈中移除。
return
以后,本地上下文也随之销毁。变量 addNumbers
也不存在了。可是,函数的定义仍然存在,它经过 retrun 语句,并赋值给了变量 adder
;这个变量,咱们是在第 3 步建立的。
来到第 10 行。在全局上下文中,咱们定义了新的变量 sum
。并分配了一个临时的值 undefined
接下来,咱们须要执行函数。哪个函数呢?就是名为 adder
的函数。咱们在全局上下文中查找它,能够保证必定能找到它。这个函数须要两个参数
咱们获得了两个参数,并把它们传递了函数。第一个是变量 val
,咱们在第 1 步定义的,它的值是数字 7
,第二个参数是数字 8
如今,咱们来调用函数。这个函数是在第 3 - 5 行被定义的。一个新的本地上下文被建立。在这个上下文中有两个新的变量:a
和 b
。它们的值分别是 7
和 8
,这就是咱们在上一步传递给函数的。
第 4 行。名为 ret
的变量被声明。它只存在本地上下文中。
第 4 行。咱们把变量 a
和 b
相加。相加后的结果(15
)赋值给了变量 ret
变量 ret
经过函数返回。随之,与之相关的本地上下文被销毁,也从调用栈中被移除,变量 a
、b
、ret
也不存在了
返回的值被赋值给咱们在第 9 步定义的变量 sum
最后,咱们在控制台输出了 sum
的值
如你所望,控制台输出的是 15。以上,咱们经历了不少。有几点我须要指出。首先,函数体的定义能够存在一个变量中,直到程序调用了函数,函数的定义是不可见的。第二,每次函数被调用,一个新的本地上下文都会被建立(临时的)。当函数执行完,随之上下文也会消失。在遇到 return
语句或者闭合的大括号 }
就说明函数结束了。
看一下下面的代码,并指出将会发生什么。
1: function createCounter() {
2: let counter = 0
3: const myFunction = function() {
4: counter = counter + 1
5: return counter
6: }
7: return myFunction
8: }
9: const increment = createCounter()
10: const c1 = increment()
11: const c2 = increment()
12: const c3 = increment()
13: console.log('example increment', c1, c2, c3)
复制代码
如今,咱们已经从前面两个示例中学到了窍门,咱们来按照以上的模式来逐步的分析下代码。
第 1 - 8 行。咱们在全局上下文中建立了新的函数变量 createCounter
第 9 行。咱们在全局上下文中声明了变量 increment
仍是第 9 行。咱们须要调用函数 createCounter
,并把结果赋值给你变量 increment
。
第 1 - 8 行。调用函数期间,会建立新的本地上下文。
第 2 行。在本地上下文中声明了变量 counter
。默认值是数字 0
。
第 3 - 6 行。声明了名为 myFunction
的变量。这个变量是在本地上下文中声明的。这个变量也是一个函数。第4 - 5 行就是相应的函数体。
第 7 行。直接放回了函数 myFunction
。本地上下文销毁。myFunction
和 counter
也伴随被销毁。从新回调了调用上下文。
第 9 行。在调用上下文中,也就是全局上下文,createCounter
返回的值赋值给了变量 increment
。此时的变量就是一个函数。这个函数是由 createCounter
返回的。虽然,不是 myFunction
,可是,函数体内容是一致的。在全局上下文中,它就是 imcrement
。
第 10 行。声明新变量(c1
)
第 10 行。调用了函数 increment
。这个函数是早期在第 4 - 5 行中定义的
建立新的上下文。只是执行函数,并无参数。
第 4 行。counter = counter + 1
。在本地上下文中查找变量 counter
。咱们只会建立上下文,绝对不会声明任何本地变量。咱们看一下全局上下文。并无变量 counter
。所以,刚才的表达式等同于 counter = undefined + 1
,声明一个本地变量 counter
,并给它赋值数字 1
,由于,undefined
有点相似 0
。
第 5 行。咱们返回了 counter
的值,也就是数字 1
。同时,销毁本地上下文和变量 counter
回到第 10 行。返回的值(1
)赋值给了 c1
第 11 行。重复第 10 - 14 步,c2
也获得数字 1
第 12 行。重复第 10 - 14 步,c3
也获得数字 1
第 13 行。咱们打印变量 c1
、c2
、c3
的值
本身试一下,看看会发生什么。你会看到,并不会像我上面说的那样输出 1
、 1
和 1
。而是,输出了 1
、2
、 3
。为何?
莫名其妙,函数 increment
记住了 counter
的值。它是如何作到的?
难道 counter
是全局上下文的一部分?试着在控制台打印 console.log(counter)
,你会看到输出 undefined
。这说明它并不在全局上下文中。
或许,当你调用 increment
时,做用域回到了函数被建立的地方(createCounter
)?怎么会呢?变量 increment
只是有着相同的函数体,并非 createCounter
。所以,也不对。
所以,必然有另一种机制。就是闭包。咱们最终说到它了。
如下就是它的工做模式。每当你声明一个新函数,并把它赋值给一个变量,用来存储函数的定义,这就是闭包。闭包包含建立函数时做用域内的全部变量。这就相似一个背包。函数定义时附带一个小背包。这个背包存储了建立函数时做用域中全部的变量。
所以,以上的分析是错误的,咱们从新正确的分析一次。
1: function createCounter() {
2: let counter = 0
3: const myFunction = function() {
4: counter = counter + 1
5: return counter
6: }
7: return myFunction
8: }
9: const increment = createCounter()
10: const c1 = increment()
11: const c2 = increment()
12: const c3 = increment()
13: console.log('example increment', c1, c2, c3)
复制代码
第 1 - 8 行。咱们在全局上下文中建立了新的函数变量 createCounter
。和上次同样
第 9 行。咱们在全局上下文中声明了变量 increment
。和上次同样
仍是第 9 行。咱们须要调用函数 createCounter
,并把结果赋值给你变量 increment
。和上次同样
第 1 - 8 行。调用函数期间,会建立新的本地上下文。和上次同样
第 2 行。在本地上下文中声明了变量 counter
。默认值是数字 0
。和上次同样
第 3 - 6 行。声明了名为 myFunction
的变量。这个变量是在本地上下文中声明的。这个变量也是一个函数。第4 - 5 行就是相应的函数体。如今,我建立了一个闭包,它是函数的一部分。闭包包含当前做用域的中的变量,在这个示例中变量是 counter
(它的值是0
)。
第 7 行。直接放回了函数 myFunction
。本地上下文销毁。myFunction
和 counter
也伴随被销毁。从新回调了调用上下文。所以,咱们获得了一个函数和闭包,这个背包中包含了函数定义时做用域中的全部变量。
第 9 行。在调用上下文中,也就是全局上下文,createCounter
返回的值赋值给了变量 increment
。此时的变量就是一个函数(也包括闭包)。这个函数是由 createCounter
返回的。虽然,不是 myFunction
,可是,函数体内容是一致的。在全局上下文中,它就是 imcrement
。
第 10 行。声明新变量(c1
)
第 10 行。调用了函数 increment
。这个函数是早期在第 4 - 5 行中定义的。(而且变量也有一个背包)
建立新的上下文。只是执行函数,并无参数。
第 4 行。counter = counter + 1
。咱们须要查询变量 counter
。在此以前,我在本地或者全局上下文中查找,此次,咱们来看一下背包,闭包。你瞧,闭包中包含一个名为 counter
的变量,它的值是 0
。通过第 4 行的计算后,它的值变成 1
。它也从新存储在背包中。这时闭包中的变量 counter
值变成了 1
。
第 5 行。咱们返回了 counter
的值,也就是数字 1
。同时,销毁本地上下文。
回到第 10 行。返回的值(1
)赋值给了 c1
第 11 行。重复第 10 - 14 步。此次,当咱们查看闭包时,看到变量 counter
的值是 1
。这是由于第 12 步致使的。这时,它的值再次被递加获得了 2
,并存储在闭包中。同时,c2
的值也是 2
。
第 12 行。重复第 10 - 14 步,c3
也获得数字 3
第 13 行。咱们打印变量 c1
、c2
、c3
的值
如今,咱们已经理解了它是如何工做的了。关键点在于,当函数被声明时,它同时包含函数体和一个闭包。这个闭包会收集建立函数时做用域中的全部变量。
你或许会问,全部的函数都有闭包吗?即便,是在全局做用域中声明?是的。全局做用建立的函数也会建立闭包。可是,因为函数是在全局做用域中被建立,所以,它们能够访问全局做用域中全部的变量。这并不彻底跟闭包有关。
当函数返回一个函数时,闭包的就比较重要了。返回的函数可访问那些不在全局做用域,只存在于闭包中的变量。
有时,在你不经意间就会出现闭包。你或许在咱们应用中看到过相似的代码。
let c = 4
const addX = x => n => n + x
const addThree = addX(3)
let d = addThree(c)
console.log('example partial application', d)
复制代码
示例中的三角函数对你来讲有点难以理解,它与下面的代码效果同样。
let c = 4
function addX(x) {
return function(n) {
return n + x
}
}
const addThree = addX(3)
let d = addThree(c)
console.log('example partial application', d)
复制代码
咱们声明了一个普通的函数 addX
,它须要一个参数 x
,并返回了另一个函数。
这个返回的函数也须要一个参数,并用它与变量 x
相加。
变量 x
就是闭包的一部分。当变量 addThree
在本地上下文中声明时,它获得了一个函数和一个闭包。闭包中就有变量 x
。
所以,当调用和执行函数 addThree
时,它能够经过闭包访问变量 x
和作为参数传递的 n
,最终返回了二者之和。
在这个示例中,控制台将会输出数字 7
。
为了记住闭包,我把它比喻为背包。当一个函数被建立并传递或者经过另一个函数返回,它就会包含一个背包。背包中包含函数声明时做用域中全部的变量。
若是,你喜欢这篇文章,不要吝啬你的赞赏 👏
谢谢