JavaScript闭包:那些年我们一起踩过的"坑"与"宝藏"

152 阅读6分钟

JavaScript闭包:那些年我们一起踩过的"坑"与"宝藏"

如果说JavaScript是一门魔法语言,那么闭包就是其中最神秘的咒语。有人说它是JavaScript的精髓,有人说它是内存泄漏的罪魁祸首。今天,让我们一起揭开闭包的神秘面纱,看看这个让无数前端工程师又爱又恨的特性到底是何方神圣。

🎭 开场白:从一个"诡异"的现象说起

先来看一段代码,猜猜会输出什么:

showName()
console.log(myname);

var myname = 'zz'
function showName() {
    console.log('函数
    showName 执行了');
}

如果你猜的是:

函数showName 执行了
undefined

那么恭喜你,你已经初步理解了JavaScript的"声明提升"机制!这就像是JavaScript引擎有一双"透视眼",能够在代码执行前就看到所有的声明。

🏗️ 基础建设:理解JavaScript的执行机制

声明提升:JavaScript的"预知能力"

核心概念 :引擎会先编译代码,再执行

JavaScript引擎就像一个勤奋的学生,总是要先预习(编译)再上课(执行)。在编译阶段,所有的 var 声明和 function 声明都会被"提升"到作用域的顶部。

调用栈:JavaScript的"记忆宫殿"

核心概念 :JS引擎追踪函数的一个机制,管理一份代码的执行关系

调用栈就像是JavaScript的"记忆宫殿",每当函数被调用时,就会在栈顶创建一个新的执行上下文。但要注意:

⚠️ 警告 :调用栈不能设计得太大,否则JS引擎在查找上下文时会花费大量时间!

来看一个递归的例子:

// 递归
function runStack(n) {
    if (n === 0) return 100
    return runStack(n - 2)
}
runStack(50000)  // 这可能会导致栈溢出!

🔍 作用域链:JavaScript的"寻宝图"

核心概念 :JS引擎在查找变量时,会先从当前作用域查找,如果没有,就会向上一级作用域查找,直到找到为止,或者直到全局作用域为止

作用域链就像一张"寻宝图",JavaScript引擎会沿着这张图一层层向上寻找变量。关键是: 作用域链的下一级是谁,是由outer指针决定的 !

让我们看一个经典的例子:

function a() {
    var num = 10
    function b() {
        var num = 20
        c()
    }
    function c() {
        console.log
        (num);  // 输出什么?
    }
    b()
}
a()  // 输出:10

为什么输出10而不是20?

因为函数 c 的outer指针指向的是函数 a 的作用域,而不是函数 b !这就是词法作用域的魅力所在。

🏠 块级作用域:let和const的"领地"

在ES6之前,JavaScript只有函数作用域和全局作用域。ES6引入了 let 和 const ,配合 {} 创造了块级作用域:

function foo() {
    var a = 1
    let b = 2
    {
        let b = 3
        var c = 4
        let d = 5
        console.log(a);  // 1
        console.log(b);  // 3
    }
    console.log(b);  // 2
    console.log(c);  // 4
    console.log(d);  // ReferenceError: d is not defined
}

看到了吗? var 声明的变量会"穿透"块级作用域,而 let 声明的变量则被"困"在了块级作用域中。

🎪 闭包:JavaScript的"魔法盒子"

终于到了今天的主角——闭包!

官方定义 :根据作用域链查找规则,内部函数一定有权访问外部函数的变量。另外,一个函数执行完毕后它的执行上下文一定会被销毁。那么当函数A内部声明了一个函数B,而函数B被拿到A的外部执行时,为了保证以上两个规则正常执行,A函数在执行完毕后会将B需要访问的变量保存在一个集合中,并留在调用栈当中,这个集合就是闭包。

听起来很抽象?让我们看一个具体的例子:

function foo() {
    var myname = 'zz'
    var age = 18

    return function bar() {
        console.log(myname);
    }
}
var baz = foo()
baz()  // 输出:zz

神奇的事情发生了!

按理说, foo 函数执行完毕后,它的执行上下文应该被销毁, myname 变量也应该不存在了。但是 baz() 依然能够访问到 myname !

这就是闭包的魔法: JavaScript引擎发现函数 bar 需要访问外部变量 myname ,于是在 foo 执行完毕后,将 myname 保存在了一个特殊的"魔法盒子"里——这就是闭包!

🕳️ 闭包的"陷阱":经典的循环问题

闭包虽然强大,但也容易让人掉坑。来看一个经典的例子:

var arr = []    // function(){} function(){} function(){}...
// 错误的写法(注释掉的部分)
// for (var i = 0; i < 5; i++) {
//     arr.push(function () {
//         console.log(i);  // 这里会输出什么?
//     })
// }

// 正确的写法:使用立即执行函数
for (var i = 0; i < 5; i++) {
    (function(j) {
        arr.push(function () 
        {
            console.log(j); // 输出 0, 1, 2, 3, 4
        })
    })(i)
}

// 执行
for (var j = 0; j < arr.length; j++) {
    arr[j]()
}

为什么需要立即执行函数?

因为如果直接在循环中创建函数,所有的函数都会共享同一个变量 i 的引用。当循环结束时, i 的值是5,所以所有函数都会输出5。

而使用立即执行函数 (function(j){...})(i) ,我们为每次循环创建了一个新的作用域,并将当前的 i 值"拷贝"给了参数 j ,这样每个函数就有了自己独立的变量副本。

⚠️ 闭包的"副作用":内存泄漏

缺点 :内存泄漏

闭包的强大之处在于它能让变量"永生",但这也是它的危险之处。如果不小心使用,闭包会导致内存泄漏:

function createHeavyObject() 
{
    var heavyData = new Array
    (1000000).fill('大量数据');
    
    return function() {
        // 即使这里不使用  heavyData
        // 但由于闭包的存在,heavyData 不会被垃圾回收
        console.log('我是一个闭包');
    }
}

var leak = createHeavyObject();
// heavyData 永远不会被释放,除非 leak = null

🎯 闭包的实际应用

1. 模块化模式

var myModule = (function() {
    var privateVar = '我是私有变量';
 
    return {
        publicMethod: 
        function() {
            return 
            privateVar;
        }
    };
})();

2. 函数柯里化

function add(a) {
    return function(b) {
        return a + b;
    }
}

var add5 = add(5);
console.log(add5(3)); // 8

3. 防抖和节流

function debounce(func, delay) {
    var timer;
    return function() {
        clearTimeout(timer);
        timer = setTimeout(func, delay);
    }
}

🎨 总结:闭包的"人生哲理"

闭包就像人生中的回忆,有些美好的时光虽然已经过去,但它们会被保存在我们心中的"闭包"里,在需要的时候重新唤起。

闭包的核心要点:

  1. 词法作用域 :函数的作用域在定义时就确定了
  2. 变量保持 :内部函数可以访问外部函数的变量
  3. 生命周期延长 :被引用的变量不会被垃圾回收
  4. 内存管理 :合理使用,避免内存泄漏

🚀 进阶挑战

理解了闭包的基本概念后,你可以尝试思考这些问题:

  1. 如何用闭包实现一个计数器?
  2. 闭包和箭头函数的this绑定有什么关系?
  3. 在React Hooks中,闭包扮演了什么角色? "闭包不仅仅是一个技术概念,它更像是JavaScript给我们的一份礼物——让我们能够创造出更加灵活和强大的代码。掌握了闭包,你就掌握了JavaScript的精髓!"

愿你在JavaScript的世界里,永远能够优雅地驾驭闭包这个强大的工具! 🎯