JavaScript 闭包详解:从原理到实战应用

作者:拾光时间:2026-10-24 21:05:46浏览:198

本文深入解析 JavaScript 中闭包的概念、形成条件及核心作用。通过作用域链和垃圾回收机制的原理,阐明闭包如何访问外部变量并延长其生命周期。文章结合具体代码示例,详细讲解利用闭包解决 for 循环中索引输出错误的三种方法:立即执行函数、let 块级作用域及自定义属性,帮助开发者正确理解并应用闭包。

1. 闭包的概念

首先通过一般函数的执行过程来引入话题:


        function stop() {
            var num = 0;
            console.log(num);
        }
        stop(); // 打印出来num是0
        console.log(num); // 报错 函数未定义

1. 在常规情况下,函数外部的代码无法直接访问函数内部定义的变量。

2. 函数内部定义的变量通常不具备持久性,它们会随着函数的执行结束而被销毁。

闭包的概念:

1. 闭包本质上是一个函数,该函数拥有访问另一个作用域中变量的权限。

2. 另一种表述方式是:当内部函数的生命周期超过外部函数的声明周期,并且内部函数以某种方式被外部作用域访问时,闭包便由此产生。

接下来通过具体的闭包代码及其解释来进行分析:


        function fn() {
            var num = 10;
            // function fun() {
            //     console.log(num);
 
            // }
            // return fun;
            return function () {
                console.log(num); // 10
            }
        }
        var f = fn();
        f();

我们可以将上述代码拆解为以下几个关键部分:

1. 函数 fn 内部包含一个返回值,且该返回值本身也是一个函数。

2. 返回的这个内部函数负责打印变量 num。之所以能够访问并打印 num,是因为 JavaScript 的作用域链访问机制,关于作用域和作用域链的具体知识点将在后文补充。

3. 我们在外部使用变量 f 接收了 fn() 的调用结果,即接收了 fn 返回的内部函数。

4. 随后调用变量 f,实际上就是调用了 fn 内部返回的那个函数。最终执行结果是打印出数字 10。

知识点的补充:

1. 作用域:

变量在特定的范围内生效,超出该范围则失效,这个范围即为作用域。作用域在函数定义时就已经确定,而非在函数调用时才产生。

2. 作用域链:

简单来说:基于【内部函数可以访问外部函数变量】这一特性,采用就近原则层层向上查找变量,这种机制被称为作用域链。

若函数 A 包含了函数 B,则函数 B 为函数 A 的内部函数,

当内部函数需要使用某个变量时,首先检查自身内部是否存在该变量,

如果不存在,则向紧邻的上一级作用域查找,遵循【就近原则】

如果层层向上都未找到,最后才会去全局作用域中查找。


        var a = 1;
        var b = 11;
        function fn1() {
            var a = 2;
            var b = '22';
            fn2();
            function fn2() {
                var a = 3;
                fn3();
                function fn3() {
                    var a = 4;
                    console.log(a); // 4
                    console.log(b); // '22'
                }
            }
        }
        fn1();

3. 垃圾回收机制

关于 JS 垃圾回收机制的详细描述,可以参考以下链接:

//www.jb51.net/article/229425.htm

结合上述三个概念,我们可以更好地理解闭包的作用。

2. 闭包的作用:

我们将外层函数称为函数 A,该函数内部包含一个函数 B。

在外部,使用一个变量 f 来接收函数 A 的返回值(即函数 B)。

而函数 A 作用域内的变量称为 num。

1. 闭包使得在函数外部能够访问函数内部的变量【搭建起外部访问内部作用域的通道】

原理:上文已有解释。

首先需要理解作用链的原理,如前所述。函数 B 能够调用函数 A 的变量 num。

其次需要理解,函数 A 的返回值是函数 B【内部函数】,且该返回值在外部被变量 f 接收。接收后调用函数 B,函数 B 便会访问函数 A 的变量 num。这个内部函数 B 即为闭包函数。

2. 能够延长函数内部变量的生命周期。

第一个作用自然导致第二个作用。JavaScript 存在垃圾回收机制,通常函数执行完毕后,变量会被清除,内存也会被释放。但如果利用闭包,变量可以不被立即清除。

原因在于,外部的变量 f 接收了函数 A 的内部函数 B,而这个内部函数访问了函数 A 作用域的变量 num。只要函数 B 被执行且变量 f 一直存在,那么变量 num 就会一直驻留在内存中,不会因为函数 A 执行结束而消失。

参考了下面的文章,讲解非常详细,推荐阅读。

JavaScript 闭包详解

3. 闭包示例

后文会补充闭包的一些具体应用场景。

我们需要明确在什么场合使用闭包,切忌滥用闭包。

3.1 点击 li 元素,输出当前 li 的索引号


    <ul class="nav">
        <li>榴莲</li>
        <li>臭豆腐</li>
        <li>鲱鱼罐头</li>
        <li>大猪蹄子</li>
    </ul>
    <script>
        // 闭包应用-点击li输出当前li的索引号
        // 1. 我们可以利用动态添加属性的方式
        var lis = document.querySelector('.nav').querySelectorAll('li');
        for (var i = 0; i < lis.length; i++) {
            lis[i].onclick = function () {
                console.log(i); // 四个4
            }
        }
    </script>

原理:如上图所示,如果直接这样写,打印出来的 i 永远都是 4。原因是,此时处于非严格模式,在非严格模式下,for 循环是同步执行的任务,而按钮点击事件的处理是异步任务。同步任务执行完毕后,i 已经增加到 4,再执行异步任务打印 i 时,结果自然都是 4。

改法 1:使用闭包

1. for 循环生成四个立即执行函数。

2. 立即执行函数是闭包的一种典型应用。立即执行函数内部的所有函数,包括【点击回调】函数,都可以使用立即执行函数传递的形参。


        for (var i = 0; i < lis.length; i++) {
            (function (i) {
                // console.log(i);
                lis[i].onclick = function () {
                    console.log(i);
 
                }
            })(i);
        }

改法 2:将 var 改为 let

点击对应的 li 元素,打印出的 i 即为对应的索引号。使用 let 是 ES6 语法,此时 for 循环具有块级作用域。


        var lis = document.querySelector('.nav').querySelectorAll('li');
        for (let i = 0; i < lis.length; i++) {
            lis[i].onclick = function () {
                // console.log(i);
                console.log(i);
            }
        }

改法 3:使用设置自定义属性 index 的方法


        var lis = document.querySelector('.nav').querySelectorAll('li');
        for (var i = 0; i < lis.length; i++) { // 注意这里是var不是let
            lis[i].index = i; // 注意这里是lis[i]不是this.index,此时没有点击,哪里来的this
            lis[i].onclick = function () {
                console.log(this.index);
            }
        }

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注本站的更多内容!

作者最新文章