一、从作用域说起:变量的 “居住法则”
(一)作用域的三种 “住所”
- 全局作用域:最顶层的 “大别墅”,用
var
/let
/const
声明的全局变量,整个程序都能访问。比如var n = 999
,在任何地方都能喊出它的名字。 - 函数作用域:函数创建的 “独立公寓”,用
var
声明的变量只能在公寓内使用。函数执行完,公寓可能被 “拆除”(变量被回收)。 - 块级作用域:ES6 新增的 “合租小房间”(
{}
内),用let
/const
声明的变量只在房间内有效,比如{ let a = 1; }
,出了房间就找不到a
啦。
(二)作用域链:变量的 “寻宝路线”
内部函数找变量时,会先在自己的 “小房间” 找,找不到就去父函数的 “公寓” 找,再找不到才去全局 “别墅” 找。比如:
var n = 999;
function f1() {
var b = 123; // 函数作用域的变量
{
let a = 1; // 块级作用域的变量
console.log(n); // 找不到a?去父函数的父级(全局)找n,输出999
}
}
二、闭包的诞生:当函数 “打包” 了变量
(一)闭包形成的三个条件
- 函数嵌套函数:儿子(内部函数)住在爸爸(外部函数)的 “公寓” 里。
- 内部函数引用外部变量:儿子偷偷拿了爸爸的 “钥匙”(引用外部变量)。
- 外部函数返回内部函数:爸爸把儿子 “送” 到外部,儿子带着钥匙走了,爸爸的公寓就没法拆啦!
(二)经典例子:闭包如何 “保存” 变量
function f1() {
var n = 999; // 自由变量,被内部函数引用
function f2() {
console.log(n); // f2形成闭包,记住了n的值
}
return f2; // 把f2交给外部
}
var result = f1(); // result就是闭包函数f2
result(); // 输出999(n还在内存里,没被回收!)
这里的n
就像被闭包 “打包” 带走了,即使f1
执行完,n
也不会被垃圾回收,因为f2
还引用着它呢~
(三)闭包的本质:作用域链的 “冻结”
闭包让外部函数的作用域在内部函数被引用时一直存活,形成一条 “冻结” 的作用域链。就像拍了张照片,把那一刻的变量状态永远保存下来。
三、闭包的两大 “超能力”
(一)让外部访问函数内部变量
正常情况下,函数内部的局部变量外部无法访问,但闭包就像开了扇 “小窗”:
function createCounter() {
var count = 0;
return {
increment: function() { count++; }, // 闭包函数
getCount: function() { return count; } // 闭包函数
};
}
var counter = createCounter();
counter.increment();
console.log(counter.getCount()); // 输出1(访问到了内部的count!)
这里通过返回对象的方法,用闭包暴露了对内部变量的操作,实现了 “私有变量” 的受控访问。
(二)让变量值 “常驻” 内存
闭包能记住每次调用后的变量状态,比如累加器:
function f1() {
var n = 999;
function nAdd() { n += 1; } // 另一个闭包函数,修改n
function f2() { console.log(n); }
return f2;
}
var result = f1();
result(); // 999
nAdd(); // 这里nAdd是全局变量(没加var,注意别这么写!)
result(); // 1000(n的值被记住了,下次调用还是1000)
闭包就像一个 “记忆面包”,让变量的值一直留在内存里,每次调用都能基于上次的状态继续操作。
四、闭包的 “副作用”:小心内存陷阱
(一)可能导致内存泄漏
如果闭包长期引用大对象或不再需要的变量,这些变量无法被垃圾回收,就会堆积在内存里,导致内存泄漏。比如:
function badClosure() {
var largeData = new Array(1000000).fill('数据'); // 大数组
return function() {
console.log(largeData.length); // 闭包一直引用largeData
};
}
var leak = badClosure(); // 即使不再用leak,largeData也无法回收
(二)如何避免内存问题
-
及时 “断舍离” :在不需要闭包时,将其设为
null
,切断引用:var result = f1(); result(); // 用完后 result = null; // 让闭包函数被回收,释放内存
-
避免不必要的全局引用:像
nAdd = function() {}
这种直接赋值给全局变量的操作要谨慎,尽量用var
声明局部变量。
(三)闭包会改变父函数内部变量
闭包在外部可以修改父函数的变量,可能带来不确定性。比如:
function f1() {
var n = 0;
function f2() { n = 10; } // 闭包修改n
return f2;
}
var fn = f1();
fn(); // n被改成10
所以如果把父函数当作 “对象”,闭包当作 “方法”,内部变量当作 “私有属性”,要小心控制修改,避免意外副作用。
五、实战案例:闭包在前端的经典应用
(一)解决this
指向问题(经典例子)
<script>
var name = 'The Window';
var object = {
name: "My Object",
getNameFunc: function() {
var that = this; // 用that保存当前this(指向object)
return function() {
return that.name; // 闭包引用that,正确获取object.name
};
}
};
console.log(object.getNameFunc()()); // 输出"My Object"
</script>
这里用闭包保存that
,避免内部函数的this
指向全局window
,是 ES6 箭头函数普及前的经典写法~
(二)模块模式:封装私有变量
var myModule = (function() {
var privateVar = '我是私有变量';
function privateFunc() { console.log('私有方法'); }
return {
publicVar: '我是公有变量',
publicFunc: function() {
privateFunc(); // 公有方法可以访问私有方法(通过闭包)
console.log(privateVar); // 也能访问私有变量
}
};
})();
myModule.publicFunc(); // 输出“私有方法”和“我是私有变量”
通过闭包,模块模式实现了私有成员和公有接口的分离,是 JS 模块化的基础思想。
六、总结:闭包是把 “双刃剑”
-
优点:实现数据封装、保存变量状态、让函数拥有 “记忆”,是 JS 实现高级功能(如模块、单例、柯里化)的核心。
-
缺点:滥用会导致内存泄漏,修改父函数变量时需谨慎控制。
理解闭包的关键,在于掌握作用域链和垃圾回收机制:当内部函数被返回并引用时,它就像背着一个 “背包”,把外部函数的变量都装了进去,走到哪儿带到哪儿。合理使用闭包,能让代码更灵活强大,但也要记得及时 “清空背包”,别让无用的变量占用内存哦~
下次遇到闭包相关的问题,想想这个 “背包客” 的比喻,是不是更清晰啦? 😉