JavaScript中的闭包
以下讨论中,Javascript均为ECMAScript的别名,仅代表ECMA-262协议的实现,不指代包括DOM、BOM在内的Web Javascript系统。
闭包概念
首先用一张图来直观看看JavaScript闭包:
将闭包比喻成一个函数携带着一个背包。这张图就很简单地描述了这种关系:闭包 = 函数 + 外部上下文
其中闭包中的这个函数一般是由另一个函数创建、返回的,这样对于这个函数来讲,外部的函数在执行时创建出来的上下文对于它就是一个外部上下文,这个外部上下文中包含了外部函数中声明的所有变量,包括闭包函数本身。
你可以将外部函数的外部函数执行时产生的上下文也理解为外部上下文,该上下文也属于闭包的一部分。层层而上,一直回溯到全局执行上下文,都是属于这个闭包。
闭包重点
- 闭包不是一个函数,而是一个函数和它的外部上下文的组合。
- 闭包的产生并不影响外部上下文的回收,闭包中的外部上下文只是在创建闭包时创建的一个副本。当然,对于同级的闭包来讲,它们会引用同一个外部上下文的副本,而不是各自创建属于自己的副本。
- Javascript中所有函数都可以理解为闭包函数,因为即使是最外层定义的函数,它也有属于自己的外部上下文——全局上下文。
- 不推荐嵌套过多层闭包,因为每个闭包创建时都会把所有外部上下文回溯到全局上下文中的变量作为属于它的外部上下文部分创建拷贝存储起来,如果嵌套层次过多会引起内存不必要的浪费。
- 同样,不推荐使用没有必要的闭包。比如在一个函数中创建了另外一个函数,但是在这个内部函数中根本就不会引用到外部这个函数中声明的变量,这个内部函数产生的闭包会保存这些没有用处的变量,造成不必要的内存浪费。
闭包作用
初学者容易从教材中混淆以下概念,认为是闭包的作用:
- 避免变量全局污染
- 使数据私有化,外部无法修改内部数据
- 可以让外部可以使用内部的私有数据
但其实这都是JavaScript中函数的作用,而并非闭包的作用。
闭包的核心作用(普通函数不具备的):
使变量可以驻留在内存,不被回收。
示例代码
let a = 100;
function fn(){
let a = 10;
a++;
console.log(a);
}
fn();
fn();
fn();
//输出11,11,11
这段代码其实没有用到闭包,完全是函数本身的作用,例如避免全局污染,数据私有化等。
那么应该如何做使得变量a常驻在内存当中呢,即每次变量a的值递增1?
let a = 100;
function fn(){
let a = 10;
return function(){
a++;
console.log(a);
}
}
let f = fn();
f();
f();
f();
//输出11,12,13
也就是在fn()函数中嵌套一个函数,用到了外层函数的变量,此时形成了闭包。
注意:除第一次
let f = fn()
外,其余三次执行函数只执行了fn()函数中的匿名函数,即没有执行let a = 10
这条语句,所以a没有被重复赋值,同时因为闭包的特点,a变量驻留在内存当中,从而可以自增。
由于闭包不影响外部上下文的回收,所以闭包本身不会造成内存泄露,但是如果闭包中引用了外部函数的变量或对象,而这些变量或对象没有被正确地释放,就会导致内存泄漏。
内存泄漏:程序在运行过程中创建的对象变量是一个内存地址(指针),若该对象重新赋值一个新的指针,而之前的指针指向的内存未进行回收,则内存泄漏。这个现象一般出现在需要手动析构对象的语言中,如C/C++。内存泄漏会导致无用的内存被占用且无法进行回收,程序长时间运行下去内存会只增不减直到内存溢出Crash。
闭包在销毁前会一直保留外部上下文中的变量,如果闭包创建过多而没有及时被GC回收,可能会造成内存溢出(Out of Memory)。
内存溢出:程序运行过程中正常分配的内存超过了运行时的最大内存,导致无法请求新的内存,运行时Crash。
如果想避免内存泄露(触发内存回收时恢复不到原点),可以手动清理闭包:f = null;