闭包:JS 里的 “背包客”,背走了变量的秘密

一、从作用域说起:变量的 "居住法则"

(一)作用域的三种 "住所"

  • 全局作用域 :最顶层的 "大别墅",用var/let/const声明的全局变量,整个程序都能访问。比如var n = 999,在任何地方都能喊出它的名字。
  • 函数作用域 :函数创建的 "独立公寓",用var声明的变量只能在公寓内使用。函数执行完,公寓可能被 "拆除"(变量被回收)。
  • 块级作用域 :ES6 新增的 "合租小房间"({}内),用let/const声明的变量只在房间内有效,比如{ let a = 1; },出了房间就找不到a啦。

(二)作用域链:变量的 "寻宝路线"

内部函数找变量时,会先在自己的 "小房间" 找,找不到就去父函数的 "公寓" 找,再找不到才去全局 "别墅" 找。比如:

javascript 复制代码
var n = 999;
function f1() {
  var b = 123; // 函数作用域的变量
  {
    let a = 1; // 块级作用域的变量
    console.log(n); // 找不到a?去父函数的父级(全局)找n,输出999
  }
}

二、闭包的诞生:当函数 "打包" 了变量

(一)闭包形成的三个条件

  1. 函数嵌套函数:儿子(内部函数)住在爸爸(外部函数)的 "公寓" 里。
  2. 内部函数引用外部变量:儿子偷偷拿了爸爸的 "钥匙"(引用外部变量)。
  3. 外部函数返回内部函数:爸爸把儿子 "送" 到外部,儿子带着钥匙走了,爸爸的公寓就没法拆啦!

(二)经典例子:闭包如何 "保存" 变量

javascript 复制代码
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还引用着它呢~

(三)闭包的本质:作用域链的 "冻结"

闭包让外部函数的作用域在内部函数被引用时一直存活,形成一条 "冻结" 的作用域链。就像拍了张照片,把那一刻的变量状态永远保存下来。

三、闭包的两大 "超能力"

(一)让外部访问函数内部变量

正常情况下,函数内部的局部变量外部无法访问,但闭包就像开了扇 "小窗":

javascript 复制代码
function createCounter() {
  var count = 0;
  return {
    increment: function() { count++; }, // 闭包函数
    getCount: function() { return count; } // 闭包函数
  };
}
var counter = createCounter();
counter.increment();
console.log(counter.getCount()); // 输出1(访问到了内部的count!)

这里通过返回对象的方法,用闭包暴露了对内部变量的操作,实现了 "私有变量" 的受控访问。

(二)让变量值 "常驻" 内存

闭包能记住每次调用后的变量状态,比如累加器:

javascript 复制代码
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)

闭包就像一个 "记忆面包",让变量的值一直留在内存里,每次调用都能基于上次的状态继续操作。

四、闭包的 "副作用":小心内存陷阱

(一)可能导致内存泄漏

如果闭包长期引用大对象或不再需要的变量,这些变量无法被垃圾回收,就会堆积在内存里,导致内存泄漏。比如:

javascript 复制代码
function badClosure() {
  var largeData = new Array(1000000).fill('数据'); // 大数组
  return function() {
    console.log(largeData.length); // 闭包一直引用largeData
  };
}
var leak = badClosure(); // 即使不再用leak,largeData也无法回收

(二)如何避免内存问题

  1. 及时 "断舍离" :在不需要闭包时,将其设为null,切断引用:

    javascript 复制代码
    var result = f1();
    result(); // 用完后
    result = null; // 让闭包函数被回收,释放内存
  2. 避免不必要的全局引用 :像nAdd = function() {}这种直接赋值给全局变量的操作要谨慎,尽量用var声明局部变量。

(三)闭包会改变父函数内部变量

闭包在外部可以修改父函数的变量,可能带来不确定性。比如:

javascript 复制代码
function f1() {
  var n = 0;
  function f2() { n = 10; } // 闭包修改n
  return f2;
}
var fn = f1();
fn(); // n被改成10

所以如果把父函数当作 "对象",闭包当作 "方法",内部变量当作 "私有属性",要小心控制修改,避免意外副作用。

五、实战案例:闭包在前端的经典应用

(一)解决this指向问题(经典例子)

html 复制代码
<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 箭头函数普及前的经典写法~

(二)模块模式:封装私有变量

javascript 复制代码
var myModule = (function() {
  var privateVar = '我是私有变量';
  function privateFunc() { console.log('私有方法'); }
  return {
    publicVar: '我是公有变量',
    publicFunc: function() {
      privateFunc(); // 公有方法可以访问私有方法(通过闭包)
      console.log(privateVar); // 也能访问私有变量
    }
  };
})();
myModule.publicFunc(); // 输出"私有方法"和"我是私有变量"

通过闭包,模块模式实现了私有成员和公有接口的分离,是 JS 模块化的基础思想。

六、总结:闭包是把 "双刃剑"

  • 优点:实现数据封装、保存变量状态、让函数拥有 "记忆",是 JS 实现高级功能(如模块、单例、柯里化)的核心。

  • 缺点:滥用会导致内存泄漏,修改父函数变量时需谨慎控制。

理解闭包的关键,在于掌握作用域链和垃圾回收机制:当内部函数被返回并引用时,它就像背着一个 "背包",把外部函数的变量都装了进去,走到哪儿带到哪儿。合理使用闭包,能让代码更灵活强大,但也要记得及时 "清空背包",别让无用的变量占用内存哦~

下次遇到闭包相关的问题,想想这个 "背包客" 的比喻,是不是更清晰啦? 😉

相关推荐
WeiXiao_Hyy2 分钟前
成为 Top 1% 的工程师
java·开发语言·javascript·经验分享·后端
吃杠碰小鸡19 分钟前
高中数学-数列-导数证明
前端·数学·算法
kingwebo'sZone25 分钟前
C#使用Aspose.Words把 word转成图片
前端·c#·word
xjt_090144 分钟前
基于 Vue 3 构建企业级 Web Components 组件库
前端·javascript·vue.js
我是伪码农1 小时前
Vue 2.3
前端·javascript·vue.js
夜郎king1 小时前
HTML5 SVG 实现日出日落动画与实时天气可视化
前端·html5·svg 日出日落
辰风沐阳1 小时前
JavaScript 的宏任务和微任务
javascript
夏幻灵2 小时前
HTML5里最常用的十大标签
前端·html·html5
冰暮流星2 小时前
javascript之二重循环练习
开发语言·javascript·数据库
Mr Xu_3 小时前
Vue 3 中 watch 的使用详解:监听响应式数据变化的利器
前端·javascript·vue.js