闭包是一个蛋疼的东西,我认为闭包不是一个新知识,而是由作用域抽象过来的,作用域才是程序语言的本质问题。

先来看个简单的例子:

  1. var parent = function () {
  2. var name = "parentname";
  3. var age = 13;
  4. var child = function () {
  5. var name = "childname";
  6. var childAge = 0.3;
  7. // => child_name 13 0.3
  8. console.log(name, age, childAge);
  9. };
  10. child();
  11. // will throw Error
  12. // ReferenceError: childAge is not defined
  13. console.log(name, age, childAge);
  14. };
  15. parent();

直觉地,内部函数可以访问外部函数的变量,外部不能访问内部函数的变量。上面的例子中内部函数 child 可以访问变量 age,而外部函数 parent 不可以访问 child 中的变量 childAge,因此会抛出没有定义变量的异常。

如果忘记var,那么变量就被声明为全局变量了。

  1. function foo() {
  2. value = "hello";
  3. }
  4. foo();
  5. console.log(value); // 输出hello
  6. console.log(global.value) // 输出hello

这个例子可以很正常的输出 hello,是因为 value 变量在定义时,没有使用 var 关键词,所以被定义成了全局变量。在 js中,全局变量会被定义在 global 对象下;在浏览器中,全局变量会被定义在 window 对象下。

如果你确实要定义一个全局变量的话,请显示地定义在 global 或者 window 对象上。

JavaScript 中,变量的局部作用域是函数级别的。不同于 C 语言,在 C 语言中,作用域是块级别的。 JavaScript 中没有块级作用域。

js 中,函数中声明的变量在整个函数中都有定义。比如如下代码段,变量 i 和 value 虽然是在 for 循环代码块中被定义,但在代码块外仍可以访问 i 和 value。

  1. function foo() {
  2.   for (var i = 0; i < 10; i++) {
  3.     var value = "hello world";
  4.   }
  5.   console.log(i); //输出10
  6.   console.log(value);//输出hello world
  7. }
  8. foo();

所以有种说法是:应该提前声明函数中需要用到的变量,即,在函数体的顶部声明可能用到的变量,这样就可以避免出现一些奇奇怪怪怪的 bug。

闭包

闭包这个概念,在函数式编程里很常见,简单的说,就是使内部函数可以访问定义在外部函数中的变量。

假如我们要实现一系列的函数:add10,add20,它们的定义是 int add10(int n)。为此我们构造了一个名为 adder 的构造器,如下:

  1. var adder = function (x) {
  2.   var base = x;
  3.   return function (n) {
  4.     return n + base;
  5.   };
  6. };
  7. var add10 = adder(10);
  8. console.log(add10(5));
  9. var add20 = adder(20);
  10. console.log(add20(5));

每次调用 adder 时,adder 都会返回一个函数给我们。我们传给 adder 的值,会保存在一个名为 base 的变量中。由于返回的函数在其中引用了 base 的值,于是 base 的引用计数被 +1。当返回函数不被垃圾回收时,则 base 也会一直存在。

闭包的一个坑

  1. for (var i = 0; i < 5; i++) {
  2.   setTimeout(function () {
  3.     console.log(i);
  4.   }, 5);
  5. }

上面这个代码块会打印五个 5 出来,而我们预想的结果是打印 1 2 3 4 5。

之所以会这样,是因为 setTimeout 中的 i 是对外层 i 的引用。当 setTimeout 的代码被解释的时候,运行时只是记录了 i 的引用,而不是值。而当 setTimeout 被触发时,五个 setTimeout 中的 i 同时被取值,由于它们都指向了外层的同一个 i,而那个 i 的值在迭代完成时为 5,所以打印了五次 5。

为了得到我们预想的结果,我们可以把 i 赋值成一个局部的变量,从而摆脱外层迭代的影响。

  1. for (var i = 0; i < 5; i++) {
  2.   setTimeout(function () {
  3.     console.log(i);
  4.   }, 5);
  5. }