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

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

内容简介:摘要:理解 JS 闭包。Fundebug经授权转载,版权归原作者所有。正如标题所述,JavaScript 闭包对我来说一直有点神秘,看过很多闭包的文章,在工作使用过闭包,有时甚至在项目中使用闭包,但我确实是这是在使用闭包的知识。

摘要:理解 JS 闭包。

Fundebug经授权转载,版权归原作者所有。

正如标题所述,JavaScript 闭包对我来说一直有点神秘,看过很多闭包的文章,在工作使用过闭包,有时甚至在项目中使用闭包,但我确实是这是在使用闭包的知识。

最近看国外的一些文章,终于,有人用于一种让我明白方式对闭包进行了解释,我将在本文中尝试使用这种方法来解释闭包。

准备

在理解闭包之前,有个重要的概念需要先了解一下,就是 js 执行上下文。

这篇 文章 是执行上下文 很不错的入门教程,文章中提到:

当代码在 JavaScript 中运行时,执行代码的环境非常重要,并将概括为以下几点:

全局代码——第一次执行代码的默认环境。

函数代码——当执行流进入函数体时。

(…) —— 我们当作 执行上下文 是当前代码执行的一个环境与范围。

换句话说,当我们启动程序时,我们从全局执行上下文中开始。一些变量是在全局执行上下文中声明的。我们称之为全局变量。当程序调用一个函数时,会发生什么?

以下几个步骤:

  • JavaScript 创建一个新的执行上下文,我们叫作本地执行上下文。
  • 这个本地执行上下文将有它自己的一组变量,这些变量将是这个执行上下文的本地变量。
  • 新的执行上下文被推到到执行堆栈中。可以将执行堆栈看作是一种保存程序在其执行中的位置的容器。

函数什么时候结束?当它遇到一个 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 引擎是如何工作的,让我们详细分析一下:

  • 在第 1 行,我们在全局执行上下文中声明了一个新变量 a,并将赋值为 3。
  • 接下来就变得棘手了,第 2 行到第 5 行实际上是在一起的。这里发生了什么? 我们在全局执行上下文中声明了一个名为 addTwo 的新变量,我们给它分配了什么?一个函数定义。两个括号{}之间的任何内容都被分配给 addTwo ,函数内部的代码没有被求值,没有被执行,只是存储在一个变量中以备将来使用。
  • 现在我们在第 6 行。它看起来很简单,但是这里有很多东西需要拆开分析。首先,我们在全局执行上下文中声明一个新变量,并将其标记为 b ,变量一经声明,其值即为 undefined。
  • 接下来,仍然在第 6 行,我们看到一个赋值操作符。我们准备给变量 b 赋一个新值,接下来我们看到一个函数被调用。当您看到一个变量后面跟着一个圆括号(…)时,这就是调用函数的信号,接着,每个函数都返回一些东西(值、对象或 undefined),无论从函数返回什么,都将赋值给变量 b 。
  • 但是首先我们需要调用标记为 addTwo 的函数。JavaScript 将在其全局执行上下文内存中查找名为 addTwo 的变量。噢,它找到了一个,它是在步骤 2(或第 2 - 5 行)中定义的。变量 add2 包含一个函数定义。注意,变量 a 作为参数传递给函数。JavaScript 在全局执行上下文内存中搜索变量 a ,找到它,发现它的值是 3,并将数字 3 作为参数传递给函数,准备好执行函数。
  • 现在执行上下文将切换,创建了一个新的本地执行上下文,我们将其命名为“addTwo 执行上下文”,执行上下文被推送到调用堆栈上。在 addTwo 执行上下文中,我们要做的第一件事是什么?
  • 你可能会说,“在 addTwo 执行上下文中声明了一个新的变量 ret”,这是不对的。正确的答案是,我们需要先看函数的参数。在 addTwo 执行上下文中声明一个新的变量 x` ,因为值 3 是作为参数传递的,所以变量 x 被赋值为 3。
  • 下一步是:在 addTwo 执行上下文中声明一个新的变量 ret 。它的值被设置为 undefined(第三行)。
  • 仍然是第 3 行,需要执行一个相加操作。首先我们需要 x 的值,JavaScript 会寻找一个变量 x ,它会首先在 addTwo 执行上下文中寻找,找到了一个值为 3。第二个操作数是数字 2。两个相加结果为 5 就被分配给变量 ret 。
  • 第 4 行,我们返回变量 ret 的内容,在 addTwo 执行上下文中查找,找到值为 5,返回,函数结束。
  • 第 4 - 5 行,函数结束。addTwo 执行上下文被销毁,变量 x 和 ret 被消去了,它们已经不存在了。addTwo 执行上下文从调用堆栈中弹出,返回值返回给调用上下文,在这种情况下,调用上下文是全局执行上下文,因为函数 addTw o 是从全局执行上下文调用的。
  • 现在我们继续第 4 步的内容,返回值 5 被分配给变量 b ,程序仍然在第 6 行。
  • 在第 7 行, b 的值 5 被打印到控制台了。

对于一个非常简单的程序,这是一个非常冗长的解释,我们甚至还没有涉及闭包。但肯定会涉及的,不过首先我们得绕一两个弯。

词法作用域(Lexical scope)

我们需要理解词法作用域的一些知识。请看下面的例子:

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 ,并给它分配一个函数定义。
  • 第六行,声明一个在全局执行上下文 multiplied 新变量。
  • 从全局执行上下文内存中查找变量 multiplyThis ,并将其作为函数执行,传递数字 6 作为参数。
  • 新函数调用(创建新执行上下文),创建一个新的 multiplyThis 函数执行上下文。
  • 在 multiplyThis 执行上下文中,声明一个变量 n 并将其赋值为 6
  • 第 3 行。在 multiplyThis 执行上下文中,声明一个变量 ret 。
  • 继续第 3 行。对两个操作数 n 和 val1 进行乘法运算.在 multiplyThis 执行上下文中查找变量 n 。我们在步骤 6 中声明了它,它的内容是数字 6。在 multiplyThis 执行上下文中查找变量 val1 。 multiplyThis 执行上下文没有一个标记为 val1 的变量。我们向调用上下文查找,调用上下文是全局执行上下文,在全局执行上下文中寻找 val1 。哦,是的、在那儿,它在步骤 1 中定义,数值是 2。
  • 继续第 3 行。将两个操作数相乘并将其赋值给 ret 变量,6 * 2 = 12,ret 现在值为 12。
  • 返回 ret 变量,销毁 multiplyThis 执行上下文及其变量 ret 和 n 。变量 val1 没有被销毁,因为它是全局执行上下文的一部分。
  • 回到第 6 行。在调用上下文中,数字 12 赋值给 multiplied 的变量。
  • 最后在第 7 行,我们在控制台中打印 multiplied 变量的值

在这个例子中,我们需要记住一个函数可以访问在它的调用上下文中定义的变量,这个就是 词法作用域(Lexical scope) 。

返回函数的函数

在第一个例子中,函数 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)

让我们回到分步分解:

  • 第一行。我们在全局执行上下文中声明一个变量 val 并赋值为 7。
  • 行 2 - 8。我们在全局执行上下文中声明了一个名为 createAdder 的变量,并为其分配了一个函数定义。第 3 至 7 行描述了上述函数定义,和以前一样,在这一点上,我们没有直接讨论这个函数。我们只是将函数定义存储到那个变量( createAdder )中。
  • 第 9 行。我们在全局执行上下文中声明了一个名为 adder 的新变量,暂时,值为 undefined。
  • 第 9 行。我们看到括号(),我们需要执行或调用一个函数,查找全局执行上下文的内存并查找名为 createAdder 的变量,它是在步骤 2 中创建的。好吧,我们调用它。
  • 调用函数时,执行到第 2 行。创建一个新的 createAdder 执行上下文。我们可以在 createAdder 的执行上下文中创建自有变量。js 引擎将 createAdder 的上下文添加到调用堆栈。这个函数没有参数,让我们直接跳到它的主体部分.
  • 第 3 - 6 行。我们有一个新的函数声明,我们在 createAdder 执行上下文中创建一个变量 addNumbers。这很重要, addnumber 只存在于 createAdder 执行上下文中。我们将函数定义存储在名为 addNumbers` 的自有变量中。
  • 在第 7 行,我们返回变量 addNumbers 的内容。js 引擎查找一个名为 addNumbers 的变量并找到它,这是一个函数定义。好的,函数可以返回任何东西,包括函数定义。我们返 addNumbers 的定义。第 4 行和第 5 行括号之间的内容构成该函数定义。
  • 返回时, createAdder 执行上下文将被销毁。 addNumbers 变量不再存在。但 addNumbers 函数定义仍然存在,因为它返回并赋值给了 adder 变量。
  • 第 10 行。我们在全局执行上下文中定义了一个新的变量 sum ,先负值为 undefined;
  • 接下来我们需要执行一个函数。哪个函数?是名为 adder 变量中定义的函数。我们在全局执行上下文中查找它,果然找到了它,这个函数有两个参数。
  • 让我们查找这两个参数,第一个是我们在步骤 1 中定义的变量 val ,它表示数字 7,第二个是数字 8。
  • 现在我们要执行这个函数,函数定义概述在第 3-5 行,因为这个函数是匿名,为了方便理解,我们暂且叫它 adder 吧。这时创建一个 adder 函数执行上下文,在 adder 执行上下文中创建了两个新变量 a 和 b 。它们分别被赋值为 7 和 8,因为这些是我们在上一步传递给函数的参数。
  • 第 4 行。在 adder 执行上下文中声明了一个名为 ret 的新变量,
  • 第 4 行。将变量 a 的内容和变量 b 的内容相加得 15 并赋给 ret 变量。
  • ret 变量从该函数返回。这个匿名函数执行上下文被销毁,从调用堆栈中删除,变量 a 、 b 和 ret 不再存在。
  • 返回值被分配给我们在步骤 9 中定义的 sum 变量。
  • 我们将 sum 的值打印到控制台。
  • 如预期,控制台将打印 15。我们在这里确实经历了很多困难,我想在这里说明几点。首先,函数定义可以存储在变量中,函数定义在程序调用之前是不可见的。其次,每次调用函数时,都会(临时)创建一个本地执行上下文。当函数完成时,执行上下文将消失。函数在遇到 return 或右括号}时执行完成。

码部署后可能存在的 BUG 没法实时知道,事后为了解决这些 BUG,花了大量的时间进行 log 调试,这边顺便给大家推荐一个好用的 BUG 监控工具Fundebug。

最后,一个闭包

看看下面的代码,并试着弄清楚会发生什么。

 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 ,变量 increment 现在包含一个函数定义内容为 createCounter 返回的函数。它不再标记为 myFunction` ,但它的定义是相同的。在全局上下文中,它是的标记为 labeledincrement 。
  • 第 10 行。声明一个新变量(c1)。
  • 继续第 10 行。查找 increment 变量,它是一个函数并调用它。它包含前面返回的函数定义,如第 4-5 行所定义的。
  • 创建一个新的执行上下文。没有参数。开始执行函数。
  • 第 4 行。counter=counter + 1。在本地执行上下文中查找 counter 变量。我们只是创建了那个上下文,从来没有声明任何局部变量。让我们看看全局执行上下文。这里也没有 counter 变量。Javascript 会将其计算为 counter = undefined + 1,声明一个标记为 counter 的新局部变量,并将其赋值为 number 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 函数记住了那个 cunter 的值。这是怎么回事?

counter 是全局执行上下文的一部分吗?尝试 console.log(counter),得到 undefined 的结果,显然不是这样的。

也许,当你调用 increment 时,它会以某种方式返回它创建的函数(createCounter)?这怎么可能呢?变量 increment 包含函数定义,而不是函数的来源,显然也不是这样的。

所以一定有另一种机制。 闭包 ,我们终于找到了,丢失的那块。

它是这样工作的,无论何时声明新函数并将其赋值给变量,都要存储函数定义和闭包。闭包包含在函数创建时作用域中的所有变量,它类似于背包。函数定义附带一个小背包,它的包中存储了函数定义创建时作用域中的所有变量。

所以我们上面的解释都是错的,让我们再试一次,但是这次是正确的。

 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 ,变量 increment 现在包含一个函数定义(和闭包),由 createCounter 返回的函数定义,它不再标记为 myFunction ,但它的定义是相同的,在全局上下文中,称为 increment 。
  • 第 10 行。声明一个新变量(c1)。
  • 继续第 10 行。查找变量 increment ,它是一个函数,调用它。它包含前面返回的函数定义,如第 4-5 行所定义的。(它还有一个带有变量的闭包)。
  • 创建一个新的执行上下文,没有参数,开始执行函数。
  • 第 4 行。counter = counter + 1,寻找变量 counter ,在查找本地或全局执行上下文之前,让我们检查一下闭包,瞧,闭包包含一个名为 counter 的变量,其值为 0。在第 4 行表达式之后,它的值被设置为 1。它再次被储存在闭包里,闭包现在包含值为 1 的变量 counter 。
  • 第 5 行。我们返回 counter的值 ,销毁本地执行上下文。
  • 回到第 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。

总结

我将永远记住闭包的方法是通过背包的类比。当一个函数被创建并传递或从另一个函数返回时,它会携带一个背包。背包中是函数声明时作用域内的所有变量。

参考

关于Fundebug

Fundebug专注于JavaScript、微信小程序、微信小游戏、支付宝小程序、React Native、Node.js和 Java 线上应用实时BUG监控。 自从2016年双十一正式上线,Fundebug累计处理了10亿+错误事件,付费客户有Google、360、金山软件、百姓网等众多品牌企业。欢迎大家免费试用!

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


以上所述就是小编给大家介绍的《我从来不理解 JavaScript 闭包,直到有人这样向我解释它...》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

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

High Performance Python

High Performance Python

Micha Gorelick、Ian Ozsvald / O'Reilly Media / 2014-9-10 / USD 39.99

If you're an experienced Python programmer, High Performance Python will guide you through the various routes of code optimization. You'll learn how to use smarter algorithms and leverage peripheral t......一起来看看 《High Performance Python》 这本书的介绍吧!

随机密码生成器
随机密码生成器

多种字符组合密码

URL 编码/解码
URL 编码/解码

URL 编码/解码

XML、JSON 在线转换
XML、JSON 在线转换

在线XML、JSON转换工具