什么是闭包?
function makeCounter() {
let count = 0;
return function() {
return count++;
};
}
let counter = makeCounter();
console.log(counter());
console.log(counter());
console.log(counter());
观察这段代码,输出结果是什么呢?基于其他编程语言的经验,可能会想当然的认为,变量count是属于makeCounter()函数的一个局部变量,当函数执行完之后,函数中的内容应随函数本身一并被销毁,那么每次得到的结果应该都是由let count = 0得出,所以输出的结果就应该是0 0 0。
但当我们运行这段代码之后,得到的结果却是0 1 2。makeCounter()明明已经返回了,为什么count不但没有被销毁,它的值反而还被保留了下来? 基于这个疑问,我们首先得到了闭包的定义,即什么是闭包:
闭包是指内部函数总是可以访问其所在的外部函数中声明的变量和参数,即使在其外部函数被返回(寿命终结)了之后。
闭包是如何产生的?
每个人都生活在一个社会环境中,同样,JavaScript的代码也存在于一个词法环境中。词法环境分为两部分:
- 对内:存储其内部属性;
- 对外:存储其外部引用;
上图是let phrase = "Hello";这段代码的词法环境。由于仅有一行代码,因此其本身就是全局词法环境,对应的外部引用即为null。接着,让我们拓展这个例子来说明,代码在执行时,词法环境是如何变化的。
上图中的编号展示了内部属性随程序执行而产生的变化。其中值得关注的是步骤①和步骤②。
- 步骤①:JavaScript代码执行前,引擎会收集程序中所有声明的变量,并将其置为未初始化(Uninitialized)状态。在该状态下,引擎仅知道变量的存在,但不能进行任何形式的使用。
- 步骤②:使用
let定义了变量phrase,但未对变量赋值,因此变量phrase的值为undefined。从这一刻开始,变量phrase才能被使用。 为了说明闭包,我们需要引入函数,让程序复杂起来。在引入函数前,需要补充一点:
函数声明不存在未初始化状态,当词法环境收集到函数声明时,该函数就可以被使用。
上面这段JavaScript代码输出的结果是Hello,John!,这里注意say()内部属性是不包括phrase的,为什么最终控制台输出phrase的值是Hello呢?这是因为,当代码要访问一个变量时,会先在内部词法环境中搜索,再搜索上一级外部环境,以此类推,直到全局词法环境。 如果所有的词法环境中都找不到这个变量,在严格模式下就会报错。因此,虽然say()的内部属性中没有phrase,但在搜索上一级词法环境时,找到了所需的phrase变量。
引入函数之后,再来看看一开始提到的闭包的例子。
在这段代码中,调用counter()时,返回函数内部属性中没有count,就从上一级词法环境中寻找并进行修改。由于counter保存了makeCounter()的返回函数,使得被保存的makeCounter()的返回函数始终是可达的,而当词法环境对象可达时,这个对象就不会从内存中被JavaScript引擎删去,makeCounter()返回函数中的内部属性和其外部引用也就被保留了下来。
从内存的角度看看闭包
JavaScript的内存模型分为三部分:代码空间,栈空间和堆空间。其中代码空间存储可执行代码,栈空间即调用栈存储执行上下文,执行上下文中仅保存7种原始类型的数据和对象类型的引用,而对象类型的“实体”则保存在堆空间中。
JavaScript引擎依赖栈空间维护的执行上下文状态,栈空间过大,会影响执行上下文的切换效率。 从内存的角度分析
makeCounter这段代码的执行流程:
- 编译
makeCounter(),创建函数执行上下文; makeCounter()中含有内部函数,对内部函数进行词法扫描,发现内部函数引用了外部函数中的变量count,JavaScript引擎就认为这是一个闭包;- JavaScript引擎在堆空间创建一个
closure(makeCounter)的对象,其中保存了count,closure(makeCounter)是个内部对象,JavaScript无法访问。
整理上述流程,产生闭包的步骤有:
- 预扫描内部函数
- 将内部函数引用的外部变量保存到堆中;
为什么要使用闭包?
- JavaScript中没有私有变量,利用闭包体现封装思想;
- 每个保存独立函数词法环境的变量,相当于利用闭包实现了函数的“实例化”;