[译]帮助你理解JavaScript中闭包的简单指引

212 阅读9分钟

闭包在JavaScript中是让很多人在他们脑中挣扎的概念之一。接下来的文章中,我将清楚的解释什么是闭包,同时我会使用简单的代码示例来直接说明。

什么是闭包?

闭包在JavaScript中是一个特性,是一个作用域链,是内部函数可以访问外部函数的变量。

闭包有三种作用域链:

  • 它可以访问自己的作用域——在它自己花括号中定义的变量
  • 它可以访问外部函数的作用域
  • 它可以访问全局变量

对于门外汉来说,这个定义看起来像全部术语了!

但是什么是真正的闭包?

一个简单的闭包

来看看JavaScript中的简单闭包:

  function outer() {
    var b = 10;
    function inner() {
      var a = 20;
      console.log(a+b);
    }
    return inner;
  }

这里我们有两个方法:

  • 外部的outer函数有一个变量b,然后返回了inner方法
  • 内部的inner函数有它自己的变量a,同时在它函数内部可以访问outer的变量b

b的作用域对outer函数是受限的,同时a的作用域对inner函数是受限的。

现在调用outer()方法,然后把outer()的结果保存在变量x中。再调用outer()方法把结果存储在Y中。

function outer() {
  var b = 10;
  function inner() {
    var a = 20;
    console.log(a+b);
  }
  return inner;
}

var X = outer(); // outer() invoked the first time
var Y = outer(); // outer() inovked the second time

让我们一步步来看当 outer() 被第一次调用的时候发生了什么:

  1. 变量 b 被创建,它的作用域被限制在 outer() 方法中,同时它的值被设置为 10
  2. 下一行是函数声明,没有什么执行。
  3. 在最后一行,return inner看起来是一个叫 inner 的变量,找到它发现这个变量 inner 实际上是一个方法,所以它返回了整个 inner 的函数体。 【注意这个 return 语句并没有执行这个inner 方法——一个方法被执行只有通过跟一个 () ——,而不是这个 return 声明返回整个函数体】
  4. 通过返回声明的年内容被存储在了X中。因此 x 保存了以下内容:
     function inner() {
       var a = 20;
       console.log(a+b);
     }
    
  5. outer() 方法执行完成,同时在 outer() 作用域内的所有变量都不再存在。

最后一部分对于理解至关重要。一旦一个函数完成了执行,在函数作用域中内部定义的的任何变量不在存在。

定义在函数内部的变量的的有效期就是函数执行的有效期。

console.log(a+b) 是什么意思,变量 b 只存在于函数 outer() 执行期间。一旦 outer() 函数执行完成,变量 b 就不在存在。

当函数第二次执行的时候,函数的变量被再次创建,而且直到函数完全执行一直存在。

因此,当 outer() 被再次调用:

  1. 一个新的变量 b 被创建,它的作用域限制在函数 outer() 中,同时它的值被设置为 10.
  2. 下一行是函数声明,没有做什么事。
  3. return inner 返回了函数 inner 的整个函数体
  4. 通过返回语句返回的内容储存在 Y 中。
  5. 函数 outer() 执行结束,outer() 作用域中的所有变量不再存在

这里有个重要的点,当 outer() 方法被第二次调用的时候,变量 b 是全新的。同样,当 outer() 方法第二次执行结束后,这个新变量 b 再次消失。

这是要意识到的最重要的点。函数内部的变量只存在于一个函数运行时,同时在函数执行完毕后立即消失。

现在,来回到示例代码去看看 XY。 既然 outer() 函数返回了一个函数,变量 XY 都是函数。

下面的JavaScript代码很容易证实:

console.log(typeof(X)); // X is of type function
console.log(typeof(Y)); // Y is of type function

因为变量 XY 都是函数,我们可以执行他们。在JavaScript中,方法执行可以通过在方法名后面添加 (),比如 X()Y()

function outer() {
  var b = 10;
  function inner() {
    var a = 20;
    console.log(a+b);
  }
  return inner;
}
var X = outer();
var Y = outer();
//end of outer() function executions
X(); // X() invoked the first time
X(); // X() invoked the second time
X(); // X() invoked the third time
Y(); // Y() invoked the first time

当我们执行 X() 或者 Y() 的时候,我们实际上在执行 inner 函数。

让我们来一步步看看当第一次执行 X() 时发生了什么:

  1. 变量 a 创建,它的值是 20
  2. JavaScript尝试执行 a+b。这就是有趣的地方。JavaScript知道 a 存在因为刚刚创建了它。然而 b 不再存在。因为 b 是外部方法的一部分, b 只有当outer() 执行的时候才存在。因为在我们调用 X() 之前,outer() 函数已经执行完了,任何在 outer 函数中的变量停止存在,因此变量 b 不再存在。

JavaScript如何处理?

闭包

由于JavaScript中的闭包,inner 函数可以访问包裹函数的变量。换句话说, inner 函数在包裹函数执行的时候保留了包裹函数的作用域,因此可以访问包裹函数的变量(译者注:函数里面包一个函数,外部的函数就叫做包裹函数)。

在我们的例子中,当 outer 函数执行时,inner 函数保留了变量 b=10,同时继续保留(闭包)它。

现在来看看它的作用域链,注意在它的作用域链中有一个变量 b,因为当 outer 函数执行时,它在那时已经在一个闭包中包裹了变量 b

因此,JavaScript知道 a=20b=10, 就可以计算 a+b

你可以通过给上面的例子添加以下代码来证明:

function outer() {
var b = 10;
   function inner() {

         var a = 20;
         console.log(a+b);
    }
   return inner;
}
var X = outer();
console.dir(X); //use console.dir() instead of console.log()

打开Chrome的元素检查,找到 Console。你可以展开这个元素看看真正的 Closure 元素(从最后一行数第三个)。注意这个变量 b=10 被保留在闭包中,即使 outer() 函数完成了它的执行。

A picture

变量 b=10 被保存在JavaScript的闭包中

现在重新回顾一下闭包的定义,我们最开始看到过,现在来看更有意义。

那么内部函数有三个作用域链:

  • 可以访问它自己的作用域——变量 a
  • 可以访问 outer 函数的变量 b,它是封闭的。
  • 可以访问任何定义的全局变量

闭包的行为

直接来看闭包,我们来通过三行代码加强下面那这个例子:

function outer() {
var b = 10;
var c = 100;
   function inner() {

         var a = 20;
         console.log("a= " + a + " b= " + b);
         a++;
         b++;
    }
   return inner;
}
var X = outer();  // outer() invoked the first time
var Y = outer();  // outer() invoked the second time
//end of outer() function executions

X(); // X() invoked the first time
X(); // X() invoked the second time
X(); // X() invoked the third time
Y(); // Y() invoked the first time

当你运行这段代码,你会在 console.log 的输出中看到如下:

a=20 b=10
a=20 b=11
a=20 b=12
a=20 b=10

来一步步检查这段代码看看究竟发生了什么,和闭包的行为!

var X = outer();  // outer() invoked the first time

函数 outer() 第一次被调用。发生下面几步:

  1. 变量 b 被创建,赋值 10

  2. 变量 c 被创建,赋值 100

    我们称 b(first_time)c(first_time) 作为自己的参考。 inner 函数返回并赋值给 x。在这一点上,变量 b 被带着 b=10 的闭包的 inner 函数的作用域链包裹着,因此 inner 使用了变量 b

  3. outer 函数完全执行完成,所有的变量不再存在。变量 c 不在存在,尽管变量 b 存在于 inner 的闭包中。

var Y= outer();  // outer() 第二次调用
  1. b 被重新创建,赋值为10

    c 被重新创建。

    注意即使 outer() 在变量 bc 消失前就执行一次,一旦函数完全执行他们就像被新创建的变量一样。

    我们称 b(second_time) c(second_time) 作为参考。

  2. inner 函数返回并且给 Y 赋值 在这一点上,变量 b 是被作为带有 b(second_time)=10 的闭包的 inner 函数的作用域链包裹着。因此, inner 可以使用变量 b

  3. outer 函数完成执行后,它的变量不再存在。

    变量 c(second_time) 不再存在,尽管变量 b(second_time) 作为闭包存在于 inner中。

现在来看看当以下代码执行后发生了什么:

X(); // X() invoked the first time
X(); // X() invoked the second time
X(); // X() invoked the third time
Y(); // Y() invoked the first time

x() 第一次调用时:

  1. 变量 a 被创建,赋值20

  2. a=20 是结果,b 的值来自闭包的值。

    b(first_time),所以 b=10

  3. 变量 ab 被加 1

  4. x() 完成执行,它内部的变量——变量 a——消失。

x() 第二次调用:

  1. 变量 a 被再次创建,设置为 20

    变量 a 之前的任何值不再存在,因为当 x() 第一次完全执行后它被消除。

  2. 变量 a=20

    变量 b 取自闭包中的值—— b(first_time)

    也要注意我们已经从之前的执行中给 b 的值增加了 1,所以 b=11

  3. 变量 ab 再次增加 1

  4. x() 方法执行完成,然后所有它内部的变量都被消除

    然而, b(first_time) 被保留下来作为闭包继续存在。

x() 第三次调用的时候:

  1. 变量 a 被重新创建,赋值 20

    变量 a 之前的任何值都不再存在,因为当 x() 第一次执行完成后就被消除。

  2. 现在的值 a=20b 的值来自于闭包的值—— b(first_time)

    同时注意,我们在之前的执行中给值增加了 1,所以 b=12

  3. 变量 ab再次加1

  4. x() 完成执行,同时它所有的内部变量——变量a——擦除。

    然而, b(first_time) 被作为闭包继续保留了下来。

当 Y()第一次被调用的时候,

  1. 变量 a 被重新创建,赋值 20

  2. 此时的值 a=20b 的值来自闭包的值——b(second_time),所以 b=10

  3. 变量 ab增加 1

  4. Y() 完成执行,所有的内部变量——变量 a ——被清除

    然而,b(second_time) 作为闭包被保留下来,所以b(second_time) 继续存在。

总结

闭包在JavaScript中不易察觉,刚开始也难理解。但是一旦理解它们,你会意识到这是一件在其他地方不存在的事。

希望这些逐步解释能帮助你真正理解JavaScript的闭包概念。

原文地址

pic