JavaScript 闭包是什么?
关于闭包,经常能看到一些比较抽象的解释,例如“函数与其词法环境的组合”。这句话本身没有错,但对于刚开始学习 JavaScript 的人来说,往往不太容易理解。
相比直接记住定义,我更喜欢通过一个简单的问题来理解:
为什么一个函数已经执行结束了,它内部的变量却还能继续被使用?
理解了这个问题,基本也就理解了闭包。
函数内部的变量,外部无法直接访问
首先来看一个简单的例子:
functiona() {
vari=0;
}
a();
console.log(i);执行后会报错:
这是因为 i 是函数 a 内部定义的局部变量,只能在 a 的作用域中访问。
函数外部无法直接访问它:
functiona() {
vari=0;
console.log(i); // 正常
}
a();
// 函数外部无法访问
console.log(i);那么,如果函数内部还有另外一个函数,而这个内部函数访问了外部函数的变量,会发生什么?
函数执行结束后,变量为什么还存在?
来看下面这个例子:
functiona() {
vari=0;
functionb() {
i++;
console.log(i);
}
returnb;
}
varres=a();
res(); // 1
res(); // 2
res(); // 3这里发生了一件很有意思的事情。
正常理解的话,a() 执行结束后,函数内部的变量 i 应该无法再直接访问了。
但实际上:
res(); // 1
res(); // 2
res(); // 3
每次调用 res(),变量 i 都会继续累加。
原因就在于,a() 返回了内部函数 b:
然后:
变量 res 保存了这个函数。
虽然外层函数 a() 已经执行结束,但内部函数 b 仍然需要访问 a() 中的变量 i。
因此,i 所在的外部词法环境仍然会被保留。
可以简单理解为:
所以每次执行:
操作的都是同一个 i:
这就是闭包最典型的表现之一。
那么,闭包到底是什么?
简单来说:
当一个函数能够访问并持续使用其外部作用域中的变量时,就形成了闭包。
重点并不只是“函数里面还有一个函数”,而是:
内部函数仍然引用了外部作用域中的变量。
即使外部函数已经执行结束,只要内部函数还存在,并且仍然需要访问这些变量,那么这些变量对应的词法环境就会继续保留。
例如前面的:
functiona() {
vari=0;
returnfunction () {
i++;
returni;
};
}返回的内部函数始终引用着 i,因此 i 可以在多次调用之间保存自己的状态。
闭包有什么用?
看到这里可能会有一个问题:
既然只是保存一个变量,直接使用全局变量不也可以吗?
例如:
var count=0;
function add() {
count++;
return count;
}
当然可以。
但全局变量的问题是,任何地方都可能直接修改它:
当项目越来越复杂时,这种变量很容易被意外修改。
闭包的一个重要作用,就是:
把数据保存在一个相对独立的作用域中,只通过指定的方法访问。
使用闭包保存状态
例如一个简单的计数器:
function createCounter() {
varcount=0;
return function () {
count++;
return count;
};
}
var counter=createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3这里外部无法直接访问:
只能通过返回的函数:
对它进行操作。
而且,每次创建的计数器都是独立的:
varcounterA=createCounter();
varcounterB=createCounter();
console.log(counterA()); // 1
console.log(counterA()); // 2
console.log(counterB()); // 1
console.log(counterB()); // 2
counterA 和 counterB 分别拥有自己的状态,互不影响。
使用闭包实现数据封装
再来看一个对象的例子。
如果直接这样定义:
varobj= {
name: "Hito",
say: function () {
console.log("My name is "+this.name);
}
};那么对象属性可以被外部直接修改:
obj.name ="Jack";
obj.say(); // My name is Jack
有时候我们并不希望外部可以随意修改某些数据。
可以使用闭包把变量保存在内部:
var obj= (function () {
var name="Hito";
return {
say: function () {
console.log("My name is "+name);
},
getName: function () {
returnname;
},
setName: function (newName) {
name=newName;
}
};
})();现在直接访问:
console.log(obj.name);
结果是:
undefined
因为真正的 name 并不是 obj 的公开属性,而是保存在函数内部:
varname="Hito";
外部只能通过提供的方法访问:
console.log(obj.getName()); // Hito
obj.setName("Jack");
console.log(obj.getName()); // Jack
这就是闭包在数据封装和私有状态中的一种应用。
虽然现在 JavaScript 已经有了 class 私有字段等新的实现方式,但闭包依然是一种非常重要的机制。
立即执行函数与闭包
早期 JavaScript 中,立即执行函数(IIFE)非常常见:
(function () {
var title=document.title;
var suffix=" - 我的博客";
document.title =title+suffix;
})();这种写法的主要作用之一,是创建一个独立的作用域,避免变量直接污染全局环境。
例如:
(function () {
var message="Hello";
})();
函数外部无法直接访问:
console.log(message);这里需要注意一点:
立即执行函数本身并不等于闭包。
IIFE 的作用主要是创建独立作用域。
是否形成闭包,关键仍然在于函数是否引用了外部作用域中的变量,并且这个引用关系持续存在。
例如:
functionouter() {
var name="Hito";
return function () {
return name;
};
}这里返回的内部函数持续访问 name,这才是一个典型的闭包场景。
现代 JavaScript 中,闭包依然无处不在
虽然现在 JavaScript 已经有:
- let
- const
- ES Module
- class
- 私有字段
很多早期依赖闭包实现的模式已经有了新的方式,但闭包本身依然是 JavaScript 非常核心的机制。
在实际开发中,我们经常会在这些场景中遇到闭包:
- 回调函数
- 事件处理
- 定时器
- Promise
- 模块化
- 函数工厂
- 状态保存
- React Hooks
例如一个事件处理函数,往往仍然能够访问它定义时所在作用域中的变量。
这也是理解 JavaScript 时非常重要的一点:
函数在哪里定义,就决定了它能够访问哪些变量。
总结
闭包并不是什么神秘的 JavaScript 特性。
可以简单理解为:
一个函数引用了外部作用域中的变量,并且这个函数在外部作用域结束后仍然可以继续访问这些变量。
通过闭包,我们可以:
- 保存函数内部状态
- 避免全局变量污染
- 实现数据封装
- 创建多个互不影响的独立状态
- 在回调和异步操作中访问外部变量
理解闭包,不只是为了回答一道 JavaScript 面试题。
更重要的是理解 JavaScript 的作用域和词法环境,理解一个函数为什么能够访问某个变量,以及这个变量为什么会在函数执行结束后仍然存在。
一旦理解了这一点,再去看回调函数、事件、异步操作甚至现代前端框架中的一些机制,也会更容易理解。
评论0
欢迎分享你的看法,也欢迎补充不同的实践经验。