# 闭包

小黄书 P45 定义:一个函数持有对声明它的函数的作用域,使得该作用域能够一直存活,这个引用就叫做闭包

红宝书 P178 定义:闭包是指有权访问另一个函数作用域中的变量的函数。创建闭包的常见方式,就是在一个函数内部创建另一个函数

MDN 定义:闭包是指那些能够访问自由变量的函数。(其中自由变量是指函数中使用的,但既不是函数参数 arguments 也不是函数的局部变量的变量,其实就是另一个函数作用域中的变量)

闭包 = 函数 + 函数能够访问的自由变量

ECMAScript中,闭包指的是:

  • 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。
  • 从实践角度:以下函数才算是闭包:
  1. 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回)
  2. 在代码中引用了自由变量

# 闭包产生的原因

作用域链 在 ES5 中有两种作用域 --- 全局作用域和函数作用域,当访问一个变量时,解释器会首先在当前作用域查找标识符,如果没有找到,就去父作用域找,直到找到该变量的标识符或者不再父作用域中,这就是作用域链。每一个子函数都会拷贝上级的作用域,形成一个作用域的链条。

var a = 1;
function f1() {
  var a = 2;
  function f2() {
    console.log(a); // 3
  }
}

f1 的作用域指向有全局作用域(window)和它本身,而 f2 的作用域指向全局作用域(window)、f1 和它本身。 作用域是从最底层开始向上找,直到找到全局作用域 window 位置,如果全局作用域还没有就会报错

闭包产生的本质就是:当前环境中存在指向父级作用域的引用 也就是在当前函数的执行上下文中维护了一个作用域链,其中会保存父级的执行上下文中的 AO/VO,即使父级函数执行完毕,执行上下文被销毁之后,但是 JavaScript 依然会让父级执行上下文中的 AO 活在内存中,当前函数依然可以通过它的作用域链找到它,正是因为这样,JavaScript 实现了闭包这个概念

function f1() {
  var a = 2;
  function f2() {
    console.log(a); // 2
  }
  return f2;
}
var x = f1();
x(); // 2

x 会拿到父级作用域中的变量,输出 2,因为在当前环境中,含有对 f2 的引用,f2 引用了 window、f1 和 f2 的作用域。因此 f2 可以访问到 f1 的作用域的变量 闭包的本质,只需要让父级作用域的引用存在,因此不一定要返回函数才算闭包,也可以:

var f3;
function f1() {
  var a = 2;
  f3 = function() {
    console.log(a);
  }
}
f1();
f3(); // 2

让 f1 执行,给 f3 赋值后,现在 f3 拥有了 window、f1 和 f3 本身这几个作用的访问权限,还是自底向上查找,最近是在 f1 中找到了 a,因此输出 2 在这里是外面的变量 f3 存在着父级作用域的引用,因此产生了闭包,形式变了但是本质没变

# 闭包的表现形式

闭包一般变现为 函数中返回一个函数,并被调用 接下来,看这道刷题必刷,面试必考的闭包题:

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = function () {
    console.log(i);
  };
}

data[0]();
data[1]();
data[2]();

答案是都是 3,让我们分析一下原因:
当执行到 data[0] 函数之前,此时全局上下文的 VO 为:

globalContext = {
    VO: {
        data: [...],
        i: 3
    }
}

当执行 data[0] 函数的时候,data[0] 函数的作用域链为:

data[0]Context = {
    Scope: [AO, globalContext.VO]
}

data[0]Context 的 AO 并没有 i 值,所以会从 globalContext.VO 中查找,i 为 3,所以打印的结果就是 3。 data[1] 和 data[2] 是一样的道理。
所以让我们改成闭包看看:

var data = [];

for (var i = 0; i < 3; i++) {
  data[i] = (function (i) {
        return function(){
            console.log(i);
        }
  })(i);
}

data[0]();
data[1]();
data[2]();

当执行到 data[0] 函数之前,此时全局上下文的 VO 为:

globalContext = {
    VO: {
        data: [...],
        i: 3
    }
}

跟没改之前一模一样。 当执行 data[0] 函数的时候,data[0] 函数的作用域链发生了改变:

data[0]Context = {
    Scope: [AO, 匿名函数Context.AO globalContext.VO]
}

匿名函数执行上下文的 AO 为:

匿名函数Context = {
    AO: {
        arguments: {
            0: 0,
            length: 1
        },
        i: 0
    }
}

data[0]Context 的 AO 并没有 i 值,所以会沿着作用域链从匿名函数 Context.AO 中查找,这时候就会找 i 为 0,找到了就不会往 globalContext.VO 中查找了,即使 globalContext.VO 也有 i 的值(值为3),所以打印的结果就是 0。 data[1] 和 data[2] 是一样的道理。