在网上看过很多前端帖子,都在问“闭包到底怎么理解”,也有不少新手朋友跟我说:面试背了不少概念,但一到看代码还是一脸懵,更别说闭包写进项目里了。说实话,闭包在JavaScript里确实是个特别容易绕晕的点,但它真没有传说中那么神。很多你天天在用的代码,其实早就悄悄用上了闭包,只是没人告诉你“这就是闭包”而已。
这篇文章会走一条跟教科书不太一样的路子:先从一个最小代码例子开始,让你真的看懂闭包长什么样;再解释它为什么能“记住”变量;最后给几个可以直接抄进项目的场景。目标只有一个:看完能用自己的话说清楚闭包,甚至能讲给旁边同事听。
1. 闭包的真实身份:从作用域聊起
1.1 先搞清楚JavaScript变量能去哪串门
理解闭包之前,得先把作用域这个前置知识点捋顺。JavaScript里,变量的可见范围是由函数边界决定的,这一点跟很多语言不太一样。一个变量定义在哪个函数内部,它就只能在那个函数内部以及更深层的函数里被访问,跑到函数外面就找不到了。
javascript复制function greet() {
var word = 'hello';
console.log(word); // 输出 hello
}
console.log(word); // 报错:word is not defined
看上面这段,word被关在greet的“房间”里了,外面代码想访问它直接报错,这种可见范围就叫作用域。JavaScript的作用域是词法作用域,说白了就是:函数能访问哪些变量,是在你写代码、定义函数的地方就决定好的,跟函数到底在哪里被调用没有关系。
这个“写在哪就决定能访问谁”的规则,是整个闭包概念的底层地基。你只要记住了这句话,后面理解闭包就是一个顺水推舟的事。
1.2 一个最简闭包:函数内部定义函数并返回
很多教材把闭包定义成“函数和它外部变量的组合”,这个说法没错,但太抽象。我更喜欢用一段能跑起来的代码来解释。
javascript复制function makeCounter() {
let count = 0;
return function () {
count++;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
这里发生了什么?makeCounter执行完之后,按一般理解,它内部的局部变量count应该被销毁才对,因为它所在函数已经执行完毕了。但是结果却显示count一直活着,每次调用counter()都能在原来基础上加一。
关键就在return function () { ... }这行:内部函数引用了外部函数的局部变量count,当这个内部函数作为返回值被扔到外面,并且被counter变量接住时,它就把count的“访问权”一起带出去了。哪怕makeCounter已经执行完,带着count的这个内部函数还在外面活着,count就会跟着这个函数继续存活。
这就是闭包的形成:函数 + 它创建时能访问到的外部变量。一个东西能把两层“房间”串起来,内外都能操作数据,这就是闭包。
为了加深印象,再看另一个容易被误解的小例子:
javascript复制function outer() {
let x = 1;
function inner() {
console.log(x);
}
inner();
}
outer();
这段代码里inner也访问了x,但它是在outer内部被调用的,并没有把函数带到外部去。严格说,这里面存在闭包所需的结构,但由于内部函数与外部作用域的引用关系在函数执行期间就结束了,它通常不会被拿出来当作“闭包应用”来讨论。如果你在面试里说这也是闭包,也不能算错,但实
