简单总结一下JavaScript中的闭包的原理。

闭包定义

简单的来说,JavaScript中的闭包可以可以定义为:可以访问外部变量的函数

那么JavaScript是如何实现这种功能的呢?

词法环境

在JavaScript内部实现中,函数或代码块实际上也是js的Object,本质上也是(key,value)的集合,局部变量的变量名就是Object内的属性名,而变量的值则是对应的属性值。对局部变量的赋值/修改操作实际上就是对于这个Object内的属性的赋值/修改操作。这种Object被称为词法环境(Lexical Environment)。

词法环境这个Object包括两部分:

  1. 环境记录(Environment Record),也就是作为局部变量来实用的Object属性
  2. 一个指向外层的词法环境的引用[[Environment]](大括号的外面一层)。当访问了本词法环境中没有的属性名的时候,js会访问这个引用,到更外层的词法环境中去查询属性名,并且这个过程会递归进行,直到查询到最外层为止。

在js中,当一个函数/代码块声明的时候,会把这个函数/代码块对应的[[Environment]]规定为本层的词法环境引用,但此时由于函数/代码块还没有实际执行,所以还没有在内存中真正创建词法环境实体,等到这个函数/代码块实际调用执行的时候,才会真正创建新的词法环境,并把这个词法环境中的[[Environment]]真正设置成之前所规定的引用值

例子

下面是一个简短的代码例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

let countBase = 0
function makeCounter() {
// 声明了makeCounter,规定了makeCounter的
// [[Environment]]指向countBase=0所在的词法环境
let count = countBase;
return function() {
// 这个函数是在makeCounter()实际执行的时候才能被声明
// 它的[[Environment]]需要执行makeCounter的时候才能确定
return count++;
};
}

{ // 新的代码块
let countBase = 10;
let counter1 = makeCounter();
let counter2 = makeCounter();
// 两次执行makeCounter()在内存中创建了两个不同的词法环境实体
// 它们所返回的两个匿名函数的[[Environment]]分别指向这两个不同的词法环境的引用
// 所以之后的counter1()与counter2()的效果是独立的

console.log( counter1() ); // 0
console.log( counter1() ); // 1
console.log( counter2() ); // 0
// 执行counter的时候没有使用countBase=10
// 这是因为匿名函数拥有makeCounter的词法环境引用
// 而makeCounter拥有指向更外层的countBase=0的词法环境的引用
}

妈的,原本想简单地总结一下的,没想到写得这么繁琐这么绕,,,

例外情况

如果使用new Function(…)来创建函数,则不满足上述的规则,new Function创建出来的函数,它们的[[Environment]]指向的是全局的词法环境(即最外层的)