JavaScript作用域与闭包:背包里的小秘密,内存泄露的“锅”谁来背?

84 阅读5分钟

程序员A:"为什么我的变量总是乱跑?"

程序员B:"因为它买了张'闭包'的环球旅行票!"

今天我们要探索JavaScript中两个神奇的概念:作用域和闭包。它们就像编程世界的"空间折叠术"——明明变量在函数里出生,却能在千里之外被召唤出来。准备好进入这个奇幻世界了吗?

🎪 作用域:变量的"活动范围"

想象变量是不同社交圈的人士:

// 1. 全局作用域 - 社交名流
var n = 999; // 所有人都认识我!

function f1() {
    // 2. 函数作用域 - 公司内部
    b = 123; // 没var声明?糟糕,我成网红了(全局变量)!
    {
        // 3. 块级作用域(ES6) - 部门小团体
        let a = 1; // 只有我们组认识我
    }
    console.log(n); // 名流谁不认识?999
}
f1();
console.log(b); // 网红果然被围观了:123

作用域链的潜规则

  • 内部可访问外部(小弟认识大哥)
  • 外部不能访问内部(大哥不认识小弟)
  • var 是社交恐惧症(函数作用域)
  • let/const 是社恐晚期(块级作用域)

注意 当我们在函数作用域内,没有用var、let、const声明变量而是直接像上述代码由于 b=123会变成全局变量,在外部可以直接访问,不需要借助闭包

垃圾回收机制

内存的生命周期 JS环境中分配的内存, 一般有如下生命周期:

  1. 内存分配:当我们声明变量、函数、对象的时候,系统会自动为他们分配内存
  2. 内存使用:即读写内存,也就是使用变量、函数等
  3. 内存回收:使用完毕,由垃圾回收器自动回收不再使用的内存
  4. 说明:
  • 全局变量一般不会回收(关闭页面回收);
  • 一般情况下局部变量的值, 不用了, 会被自动回收掉

🎒 闭包:神奇的"背包"

闭包不是魔术,但比魔术更神奇!它能让局部变量"偷渡"到外部:

function f1() {
    var n = 999; // 本该消失的局部变量
    function f2() {
        console.log(n); // 抓住n不放!
    }
    return f2; // 把f2当"人质"带出去
}

// 执行魔法
const secretBag = f1(); 
secretBag(); // 999!变量复活了!

但是在闭包中,我们f1执行完,其变量n为什么没有被回收呢?那是因为我们 f2还存在对f1函数中变量n的引用,简单来说闭包会阻止外部还是变量的回收

闭包三要素

  1. 函数嵌套函数(背包套小包)
  2. 内部函数访问外部变量(小包装了大包的东西)
  3. 外部函数返回内部函数(把背包递出去)

🧪 闭包实验室:长生不老的变量

// 3.js
function f1() {
    var n = 999;
    // 全局函数偷偷修改闭包变量
    nAdd = function() { n += 1 };
    function f2() { console.log(n); }
    return f2;
}

const result = f1();
result(); // 999
nAdd();   // 幕后黑手操作
result(); // 1000!变量竟然记住了变化!

实验结论

  • 闭包让局部变量"长生不老"
  • 被引用的变量不会被垃圾回收
  • 可随时通过闭包函数访问
  • 甚至能被外部函数修改(nAdd

同时这也充分的说明了外部函数的自由变量不会被销毁

💡 闭包为什么叫"背包"?

想象外部函数是游客,内部函数是背包:

  • 游客走了(函数执行完毕)
  • 但背包被留下了(返回的函数)
  • 背包里装着游客的纪念品(局部变量)
  • 纪念品随时可取出(通过闭包访问)

希望这个比喻能帮助你更好地理解闭包阻止了变量地销毁

🎯 this指向:闭包里的"身份迷失"

<!-- 4.html -->
<script>
var name = 'The Window';
var obj = {
    name: 'My object',
    getNameFunc: function() {
        var that = this; // 快照!保存当前身份
        return function() {
            return that.name; // 正确:My object
            // return this.name; // 错误:The Window
        };
    }
};
console.log(obj.getNameFunc()()); 
</script>

this的犯罪现场分析

  • 直接返回this时:匿名函数的this指向window
  • 闭包导致this丢失原对象绑定
  • 破案技巧:用that保存正确的this

返回的匿名函数调用,相当于普通函数的执行,this是指向最后调用它的,而普通函数的this是指向window的,所以this就是window,所以这里我们打印的是全局变量

如果大家对this有疑惑的,可以看看我的文章JavaScript中的this指向:从懵圈到豁然开朗的奇幻之旅 相信看完大家心中的疑虑会解决不少

程序员吐槽:"闭包里的this,就像在迪士尼乐园穿西装的打工人——外表光鲜,实际早已迷失自我!"

🚨 闭包的危险:内存泄露的"锅"

闭包虽好,但可能变"内存杀手":

// 危险操作示范
function createHeavyClosure() {
    const bigData = new Array(1000000); // 超大数组
    return function() {
        console.log(bigData.length); 
    };
}
const heavyBag = createHeavyClosure();
// 即使不再需要,bigData仍被闭包引用!

内存泄露预防指南

  1. 及时清理:heavyBag = null
  2. 避免循环引用
  3. 使用弱引用:WeakMap/WeakSet
  4. 模块化设计,限制闭包范围

💎 总结:闭包使用三原则

  1. 必要才用:如非必须,勿增闭包
  2. 及时清理= null 是好习惯
  3. 明确边界:避免无节制暴露内部状态

最后友情提示:闭包就像辣椒——适量提味,过量烧胃。现在,是时候打开你的编辑器,用闭包写点"魔法代码"了!毕竟,不会用闭包的JavaScript程序员,就像不会用筷子的中餐厨师——能活,但憋屈!