闭包 —— 这个让无数前端 er 抓秃头皮的 JavaScript 概念😫,堪称面试中的 “常驻嘉宾”。有人说它像 “幽灵”,变量明明该销毁却一直存在;有人说它像 “背包”,总能带走函数里的 “宝贝”。今天,咱们就拆透它的本质,看完这篇,闭包考点再也难不倒你!
一、闭包到底是个啥?一句话讲透 🤫
闭包的本质,是 **“函数嵌套 + 作用域链嵌套” 形成的特殊结构 **。它就像一座隐形的桥梁🌉,把函数内部和外部死死连在一起 —— 哪怕外部函数执行完了,内部函数依然能拽着外部的变量不放。
阮一峰老师在文章里说得更直白:“闭包是将函数内部和函数外部连接起来的桥梁”。
形象点说:闭包就是个 “随身背包”🎒,内部函数被返回时,会把外部函数的变量 “打包” 带走,让这些变量逃过垃圾回收的 “魔爪”。
二、闭包形成的 3 个硬条件(缺一不可)✅
不是随便嵌套个函数就是闭包!必须同时满足这 3 个条件,闭包才能 “生效”:
- 函数要嵌套:内部函数得乖乖待在外部函数的 “肚子里”(比如
f2在f1内部); - 内部函数得 “惦记” 外部变量:内部函数必须用到外部函数的局部变量(这些变量叫 “自由变量”);
- 内部函数要 “逃出去” 被调用:外部函数得把内部函数 “放出去”(return),并且这个 “逃出去” 的函数还得在外部被执行。
🌰 举个最经典的例子:
function f1() {
var n = 999; // 外部函数的局部变量(自由变量)
function f2() { // 内部函数(闭包函数)
console.log(n); // 内部函数“惦记”外部变量
}
return f2; // 把内部函数“放出去”
}
// 外部函数执行,拿到“逃出去”的内部函数
var result = f1();
// 外部调用内部函数,闭包生效!
result(); // 输出 999(居然能拿到f1里的n!)
三、闭包为啥能 “留住” 变量?揭秘底层逻辑 🧠
很多人疑惑:外部函数都执行完了,它的变量为啥没被销毁?这得从两个点说起:
1. 作用域链:变量查找的 “地图”
JavaScript 里,函数内部能访问外部变量,靠的是 “作用域链”—— 每个函数都有自己的作用域,嵌套函数的作用域链会 “套娃”,内部函数能顺着链条找到外部的变量。
比如下面例子里的作用域嵌套:
var n = 999; // 全局作用域
function f1() {
b = 123; // 没加var/let/const,自动成全局变量(文档5知识点)
{
let a = 1; // 块级作用域(只在{}里有效)
}
console.log(n); // 内部能访问外部的n(作用域链生效)
}
f1(); // 输出 999
闭包就是利用了这种 “嵌套的作用域链”,让内部函数在外部执行时,依然能顺着链条找到外部变量。
2. 垃圾回收:“引用计数” 在搞鬼
JavaScript 的垃圾回收机制有个 “引用计数” 规则:只要变量被引用着,就不会被销毁。
闭包里的自由变量,因为被内部函数 “惦记”(引用),哪怕外部函数执行完了,只要内部函数还在被使用,这些变量就会一直 “赖” 在内存里。
🌰 验证一下:
function f1() {
let n = 999;
// 全局函数nAdd,修改n(闭包外部也能改内部变量)
nAdd = function() { n += 1; };
function f2() { console.log(n); }
return f2;
}
var result = f1();
result(); // 第一次调用:输出 999
nAdd(); // 外部修改n
result(); // 第二次调用:输出 1000(n果然没被销毁!)
两次调用result(),n 的值从 999 变成 1000,完美证明:闭包让变量 “常住” 内存了!
四、闭包能干嘛?3 大实用场景 🔧
闭包不是花架子,实际开发中超有用!这 3 个场景一定要记住:
1. 让外部 “偷看” 函数内部的变量
函数内部的局部变量,本来是 “私有” 的,外部拿不到。但闭包能开个 “小窗口”,让外部间接访问。
比如上面例子的核心作用:通过返回内部函数,让全局能访问f1里的n。
2. 让变量 “常驻内存”,保存状态
有些场景需要变量一直 “活着”(比如计数器),闭包就能做到。
🌰 实现一个自增计数器:
function createCounter() {
let count = 0; // 这个变量会被闭包“留住”
return function() {
return count++;
};
}
const counter = createCounter();
console.log(counter()); // 0
console.log(counter()); // 1(count一直保存在内存里)
3. 隔离作用域,避免全局变量污染
全局变量多了容易冲突,闭包能创建 “独立小房间”,把变量关在里面。
🌰 举个经典案例:
<script>
var name = "The Window"; // 全局变量
var object = {
name: "My Object",
getNameFunc: function() {
var that = this; // 保存当前this(指向object)
// 返回闭包函数,能访问外部的that
return function() {
return that.name;
};
}
};
// 输出"My Object",而不是全局的"The Window"
console.log(object.getNameFunc()());
</script>
这里用闭包 “锁住” 了that(指向object),完美避免了this指向全局的坑!
五、闭包的 “坑”:3 个必须注意的点 ⚠️
闭包虽香,但也有副作用,踩坑了可能出大问题!
1. 可能导致内存泄漏
变量一直 “赖” 在内存里不走,积累多了会让页面变卡。
解决办法:不用的时候,手动 “赶走” 变量 —— 把闭包引用设为null:
var result = f1();
result();
result = null; // 手动切断引用,让垃圾回收机制回收变量
2. 可能偷偷修改父函数的变量,引发混乱
闭包不仅能读变量,还能改!如果多个地方改同一个变量,容易出 bug。
🌰 小心这种情况:
function f1() {
let n = 0;
return {
add: () => n++,
log: () => console.log(n)
};
}
const obj = f1();
obj.add();
obj.log(); // 输出1(n被偷偷改了)
用的时候一定要理清楚变量的修改逻辑!
3. 自由变量的生命周期 “不受控”
自由变量的 “生死”,不取决于父函数啥时候执行完,而取决于闭包啥时候被销毁。这可能导致变量意外保留状态,比如:
function f() {
let arr = [];
return function(num) {
arr.push(num);
console.log(arr);
};
}
const add = f();
add(1); // [1]
add(2); // [1,2](arr一直被保留,哪怕f早就执行完了)
六、总结:闭包是把 “双刃剑” 🗡️
闭包让函数内部和外部能 “沟通”,能保存状态、隔离作用域,是前端开发的 “利器”;但也可能导致内存泄漏、变量混乱,用的时候得小心。
记住一句话:理解作用域链和垃圾回收,就理解了闭包的一半;多练案例,就能吃透另一半。
下次面试再被问闭包,就把这篇文章的知识点甩出来 —— 保证面试官点头称赞!😎