闭包(Closure)是JavaScript中的一个重要概念,它允许内部函数访问其外部函数的变量,即使在外部函数执行完毕后仍然可以访问。闭包的核心在于它维持了对包含它的词法环境(作用域链)的引用,这样内部函数就能“记住”并访问外部作用域中的变量。
作用域链:每个函数在执行时都有一个作用域链,这个链包含了函数自身的作用域和包含它的作用域。当函数查找一个变量时,首先在自己作用域中查找,没有就向上搜索作用域链。
结合例子
function createAdder(a) {
return function add(b) {
return a + b;
};
}
在这个例子中,`createAdder`函数返回了一个内部函数`add`。这个`add`函数就是一个闭包,原因如下:
1. 访问外部作用域变量:`add`函数在其定义时,能够访问其外部函数`createAdder`的作用域中的变量`a`。即使在`createAdder`执行完毕后,`add`依然能够记住并访问到这个`a`的值。至于为什么`add`能够记住这个`a`值,就要提到下一个特性
const addTo5 = createAdder(5); // 外部函数createAdder执行完毕
console.log(addTo5(3)); // 输出8,这里尽管createAdder已经执行完毕,但a=5的值被add(作为闭包)保留了下来,并与后来传入的b=3相加。
2. 维持状态:闭包的一个关键用途是维持状态或封装变量。
当一个外部函数执行结束并且将其内部变量通常应该销毁时,只要内部函数(闭包)还被引用,外部函数的作用域就不会被销毁。内部函数维持了一个对其外部作用域的引用,这样就可以继续访问那些“应该已经消失”的外部变量。就是上文提到的作用域链,闭包内部作用域找不到的变量,会向外部函数中的作用域查找,就使得外部函数的作用域得以保留。
在这个例子中,`a`的值(5)在`createAdder(5)`调用时确定,并通过闭包机制被`add`函数保留下来。这意味着每次通过`addTo5(某个数)`调用时,实际上都是在原来的基础上(5)加上新的数,而不会丢失最初传给`createAdder`的`a`值,计算出`5+3=8`的结果。这就是“在其外部函数执行完毕后仍可以访问”的含义。
3.数据封装与隐藏
正常情况下,当外部函数执行完毕后,按照一般规则,它的执行环境(包括局部变量)理应被垃圾回收机制回收,因为理论上不再有引用指向这些资源。但是上一个特性说到,存在闭包调用外部函数变量时,因为作用域链,本该销毁的外部函数作用域不会消失,闭包会维持对外部函数作用域中变量的引用。换句话说就是因为闭包而延长了“生命期”,外部函数作用域中变量`a`并没有被立即销毁,直到不再有任何引用指向闭包,导致闭包本身也成为垃圾回收的目标时,这些外部变量才会最终被销毁。这就是闭包能够维持外部函数作用域中变量状态的原因。所以这些变量可以被视为闭包的“私有变量”,因为它们对外部不可直接访问,且生命周期超出了原本预期的范围。