我历来都不理解闭包

原文地址: 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

  1. JavaScript 会建立一个本地的全新上下文
  2. 本地的上下文会有它内部的变量,这些变量属于当前的本地执行上下文
  3. 新的执行上下文将会被压入执行栈中。执行栈就是为了跟踪程序在哪执行的机制。

函数什么时候结束?当遇到 return 语句或者遇到闭合的大括号 }。当函数结束时,会依次发生如下状况:

  1. 本地执行上下文会从执行栈中被弹出
  2. 调用上下文将会获得函数的返回值。调用上下文就是函数被调用时的执行上下文,它但是全局执行上下文或者另一个本地执行上下文。此时,调用上下文会处理函数的返回值。返回值能够是对象、数组、函数、布尔值、任何值。若是,函数没有 return 语句,默认,将会返回 undefined
  3. 接下来,本地执行上下文将会被销毁。这个很重要。销毁表明着全部在其内部声明的变量都会被抹除。它们再也不可用。这也是为何把它们称为本地变量。

一个简单的演示

解释闭包以前,咱们先看一下如下的代码片断。它看起很是简单直接,任何人都知道会发生什么。

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 引擎是如何工做的,咱们来逐步分析一下:

  1. 在第一行中,咱们在全局执行上下文中声明了一个变量 a 并赋值数字 3

  2. 接下来会变得棘手。第二行到第五行是一个总体。发生了什么呢?在全局执行上下文中咱们声明了一个名为 addTwo 的新变量。咱们给它分配什么呢?函数定义。两个大括号 { } 任何代码都会分配给 addTwo。函数内部的代码,这时并不会被计算,也不会执行,只是存储在一个变量中以便未来使用。

  3. 如今,咱们来到了第六行。看起很是简单,可是,这里包含了不少东西。首先,在全局执行上下文中,咱们声明了一个新的变量 b。变量声明的同时,也会赋值 undefined

  4. 接下来,仍然是第六行,咱们看到有一个赋值操做符。这时,咱们才真正赋值给变量 b 。接下来,咱们看到函数被调用了。当你看到一个变量后面跟着一个圆扣号 (),那表明着函数调用执行。如前所述,每一个函数都会返回一些值(值、对象或者 undefined)。无论,函数返回什么都会赋值给变量 b

  5. 可是,首先咱们须要调用函数 addTwo。JavaScript 将会在全局执行上下文中查找一个名为 addTwo 的变量。是的,它找到了,在第二步定义的(第二行到第五行)。你看,变量 addTwo 是一个函数。注意,变量 a 做为一个参数传递给了函数。JavaScript 会在全局执行上下文中搜索变量 a 并找到它,发现它的值是 3,而后,数字 3 就传递给了函数。准备开始执行函数。

  6. 如今,执行上下文发生了改变。一个新的本地执行上下文被建立,咱们叫它 “addTwo 执行上下文”。这个执行上下文被压入到调用栈。在本地执行上下文中咱们首先要作什么呢?

  7. 你可能会说:“一个新的变量 ret本地执行上下文中被声明了”。这不对,正确的答案是,首先,咱们须要看一下函数的参数。在本地执行上下文中声明了一个新的变量 x 。而后,因为数字 3 作为参数传递给了函数,那么,变量 x 就的值就变成了 3

  8. 下一步:本地执行上下文声明了新的变量 ret。它的值是 undefined(第三行)

  9. 仍旧是第三行,须要执行加法。首先,咱们须要用到 x 的值。JavaScript 将会查找变量 x。首先,它会在本地执行上下文中查找。并且,找到了,它的值是 3。第二个操做数是数字 2。二者相加以后的结果(5)将会赋值给变量 ret

  10. 第四行。咱们会返回变量 ret。另外,根据本地执行上下文的内容得知 ret 的值是 5。函数将会返回数字 5。这时,函数结束。

  11. 函数在第四到第五行结束。本地执行上下文也随之被销毁。变量 xret 同时被抹除。它们将会消失。调用栈也会弹出响应的上下文,返回值将会返回到调用上下文。在这个案例中,调用栈就是全局执行上下文,这是由于,函数 addTwo 是在全局执行上下文中被调用的。

  12. 如今,咱们从新回到第四步。返回值(数字5)赋值给了变量 b。咱们仍旧在程序的第六行。

  13. 我不用详解介绍,在第七行,变量 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)。根据这个规则,上面的示例就很清晰了。若是,你清楚做用域是如何工做的,你能够跳过这部分。

  1. 在全局执行上下文中声明一个变量 val1,而后,给它赋值数字 2

  2. 第 2 - 5 行。声明了一个新变量 multiplyThis,而后,定义了一个函数

  3. 第 6 行。在全局执行上下文声明了一个变量 multiplied

  4. 在全局执行上下文中找到变量 multiplyThis,并作为一个函数执行。而后,把数字 6 作为参数传递给函数

  5. 函数被调用 = 新的执行上下文。建立新的本地执行上下文

  6. 在本地执行上下文中,声明了变量 n 并赋值了数字 6

  7. 第 3 行。声明了变量 ret

  8. 第 3 行。变量 nvall 两个数的相乘。在本地执行上下文中查找变量 n。咱们在第 6 行声明了这个变量。它的值是数字 6。本地上下文中没有找到变量 vall。须要检测调用上下文。由于,调用上下文是全局上下文。咱们须要在全局上下文中查找 vall。很好,找到了。它在第 1 行被定义的。它的值是数字 2

  9. 第 3 行。两个数相差,而后赋值给变量 ret。6 * 2 = 12。ret 的值是 12

  10. 返回 ret 的值。随之本地上下文也被销毁,同时销毁的还有变量 retn。变量 vall 并不会被销毁,由于它是全局上下文的一部分。

  11. 回到第 6 行。在调用上下文中,数字 12 被复制给变量 multiplied

  12. 最后的第 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. 第 1 行。咱们在全局上下文中声明了变量 val,而且赋值数字 7 给变量

  2. 第 2 - 8 行。在全局上下文中咱们声明了一个名为 createAdder 的函数。第 3 - 7 行就是函数的具体定义。和以前同样,这个时候,咱们并不会执行函数。咱们只是把函数赋值给一个变量(createAdder

  3. 第 9 行。在全局上下文中,咱们声明了新的变量 adder。同时,它的值是 undefined

  4. 仍是第 9 行。咱们看到了一个圆括号();表明着咱们须要调用函数。咱们在全局上下文中搜索找了到名为 createAdder 的变量。它是在第 2 步建立的。好,咱们来调用它。

  5. 调用函数。如今,咱们回到第 2 行。一个新的上下文被建立。在新的上下文中咱们建立了本地变量。同时,引擎也会把新的上下文压入到调用栈。这个函数没有参数,咱们直接看它的内部。

  6. 第 3 - 6 行。咱们又声明了一个新的函数。在本地上下文中咱们建立变量 addNumber。这个很重要。addNumber 只在本地上下文中有效。在本地上下文中咱们定义了一个函数并命名为 addNumber

  7. 如今,我来到第 7 行。咱们返回了变量 addNumber。引擎会查找变量 addNumber,固然也会找到它。它是一个函数。好,函数能够返回任何东西,包括函数。所以,咱们返回了 addNumbers 的函数体。在第 4 - 5 行就是函数的具体定义。同时,咱们也把本地上下文从调用栈中移除。

  8. return 以后,本地上下文也随之销毁。变量 addNumbers 也不存在了。可是,函数的定义仍然存在,它经过 retrun 语句,并赋值给了变量 adder;这个变量,咱们是在第 3 步建立的。

  9. 来到第 10 行。在全局上下文中,咱们定义了新的变量 sum。并分配了一个临时的值 undefined

  10. 接下来,咱们须要执行函数。哪个函数呢?就是名为 adder 的函数。咱们在全局上下文中查找它,能够保证必定能找到它。这个函数须要两个参数

  11. 咱们获得了两个参数,并把它们传递了函数。第一个是变量 val,咱们在第 1 步定义的,它的值是数字 7,第二个参数是数字 8

  12. 如今,咱们来调用函数。这个函数是在第 3 - 5 行被定义的。一个新的本地上下文被建立。在这个上下文中有两个新的变量:ab。它们的值分别是 78,这就是咱们在上一步传递给函数的。

  13. 第 4 行。名为 ret 的变量被声明。它只存在本地上下文中。

  14. 第 4 行。咱们把变量 ab 相加。相加后的结果(15)赋值给了变量 ret

  15. 变量 ret 经过函数返回。随之,与之相关的本地上下文被销毁,也从调用栈中被移除,变量 abret 也不存在了

  16. 返回的值被赋值给咱们在第 9 步定义的变量 sum

  17. 最后,咱们在控制台输出了 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. 第 1 - 8 行。咱们在全局上下文中建立了新的函数变量 createCounter

  2. 第 9 行。咱们在全局上下文中声明了变量 increment

  3. 仍是第 9 行。咱们须要调用函数 createCounter,并把结果赋值给你变量 increment

  4. 第 1 - 8 行。调用函数期间,会建立新的本地上下文。

  5. 第 2 行。在本地上下文中声明了变量 counter。默认值是数字 0

  6. 第 3 - 6 行。声明了名为 myFunction 的变量。这个变量是在本地上下文中声明的。这个变量也是一个函数。第4 - 5 行就是相应的函数体。

  7. 第 7 行。直接放回了函数 myFunction。本地上下文销毁。myFunctioncounter 也伴随被销毁。从新回调了调用上下文。

  8. 第 9 行。在调用上下文中,也就是全局上下文,createCounter 返回的值赋值给了变量 increment。此时的变量就是一个函数。这个函数是由 createCounter 返回的。虽然,不是 myFunction,可是,函数体内容是一致的。在全局上下文中,它就是 imcrement

  9. 第 10 行。声明新变量(c1

  10. 第 10 行。调用了函数 increment。这个函数是早期在第 4 - 5 行中定义的

  11. 建立新的上下文。只是执行函数,并无参数。

  12. 第 4 行。counter = counter + 1。在本地上下文中查找变量 counter。咱们只会建立上下文,绝对不会声明任何本地变量。咱们看一下全局上下文。并无变量 counter。所以,刚才的表达式等同于 counter = undefined + 1,声明一个本地变量 counter,并给它赋值数字 1,由于,undefined 有点相似 0

  13. 第 5 行。咱们返回了 counter 的值,也就是数字 1。同时,销毁本地上下文和变量 counter

  14. 回到第 10 行。返回的值(1)赋值给了 c1

  15. 第 11 行。重复第 10 - 14 步,c2 也获得数字 1

  16. 第 12 行。重复第 10 - 14 步,c3 也获得数字 1

  17. 第 13 行。咱们打印变量 c1c2c3 的值

本身试一下,看看会发生什么。你会看到,并不会像我上面说的那样输出 111。而是,输出了 123。为何?

莫名其妙,函数 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. 第 1 - 8 行。咱们在全局上下文中建立了新的函数变量 createCounter。和上次同样

  2. 第 9 行。咱们在全局上下文中声明了变量 increment。和上次同样

  3. 仍是第 9 行。咱们须要调用函数 createCounter,并把结果赋值给你变量 increment。和上次同样

  4. 第 1 - 8 行。调用函数期间,会建立新的本地上下文。和上次同样

  5. 第 2 行。在本地上下文中声明了变量 counter。默认值是数字 0。和上次同样

  6. 第 3 - 6 行。声明了名为 myFunction 的变量。这个变量是在本地上下文中声明的。这个变量也是一个函数。第4 - 5 行就是相应的函数体。如今,我建立了一个闭包,它是函数的一部分。闭包包含当前做用域的中的变量,在这个示例中变量是 counter(它的值是0)。

  7. 第 7 行。直接放回了函数 myFunction。本地上下文销毁。myFunctioncounter 也伴随被销毁。从新回调了调用上下文。所以,咱们获得了一个函数和闭包,这个背包中包含了函数定义时做用域中的全部变量。

  8. 第 9 行。在调用上下文中,也就是全局上下文,createCounter 返回的值赋值给了变量 increment。此时的变量就是一个函数(也包括闭包)。这个函数是由 createCounter 返回的。虽然,不是 myFunction,可是,函数体内容是一致的。在全局上下文中,它就是 imcrement

  9. 第 10 行。声明新变量(c1

  10. 第 10 行。调用了函数 increment。这个函数是早期在第 4 - 5 行中定义的。(而且变量也有一个背包)

  11. 建立新的上下文。只是执行函数,并无参数。

  12. 第 4 行。counter = counter + 1。咱们须要查询变量 counter。在此以前,我在本地或者全局上下文中查找,此次,咱们来看一下背包,闭包。你瞧,闭包中包含一个名为 counter 的变量,它的值是 0。通过第 4 行的计算后,它的值变成 1。它也从新存储在背包中。这时闭包中的变量 counter 值变成了 1

  13. 第 5 行。咱们返回了 counter 的值,也就是数字 1。同时,销毁本地上下文。

  14. 回到第 10 行。返回的值(1)赋值给了 c1

  15. 第 11 行。重复第 10 - 14 步。此次,当咱们查看闭包时,看到变量 counter 的值是 1。这是由于第 12 步致使的。这时,它的值再次被递加获得了 2,并存储在闭包中。同时,c2 的值也是 2

  16. 第 12 行。重复第 10 - 14 步,c3 也获得数字 3

  17. 第 13 行。咱们打印变量 c1c2c3 的值

如今,咱们已经理解了它是如何工做的了。关键点在于,当函数被声明时,它同时包含函数体和一个闭包。这个闭包会收集建立函数时做用域中的全部变量。

你或许会问,全部的函数都有闭包吗?即便,是在全局做用域中声明?是的。全局做用建立的函数也会建立闭包。可是,因为函数是在全局做用域中被建立,所以,它们能够访问全局做用域中全部的变量。这并不彻底跟闭包有关。

当函数返回一个函数时,闭包的就比较重要了。返回的函数可访问那些不在全局做用域,只存在于闭包中的变量。

非同小可的闭包

有时,在你不经意间就会出现闭包。你或许在咱们应用中看到过相似的代码。

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

总结

为了记住闭包,我把它比喻为背包。当一个函数被建立并传递或者经过另一个函数返回,它就会包含一个背包。背包中包含函数声明时做用域中全部的变量。

若是,你喜欢这篇文章,不要吝啬你的赞赏 👏

谢谢

相关文章
相关标签/搜索