js高级篇:什么是闭包?闭包有什么用?面试时如何处理闭包问题?

本文详细解析了JavaScript闭包的概念,包括定义、作用、用法及应用实例,并探讨了闭包在解决实际问题中的优势与局限性。

须知:我们在学一个新的东西的时候,例如我们第一次见到电饭煲,我们要知道什么是电饭煲(定义),知道电饭煲有什么用(作用),知道电饭煲怎么用(用法),如何用电饭煲煮一顿好吃的饭(实践),做出来之后你就真正懂了电饭煲。学习闭包的过程亦是如此,所以我们不用害怕学习新事物!

1.闭包的定义

  • 定义:闭包是指有权访问另一个函数作用域中的变量的一个函数。简单的说,你可以认为闭包是一个特别的函数,他能够读取其他函数内部变量的函数。
  • 自由变量:在当前作用域没有定义,在父作用域及以上定义了,他会一层一层的往上找。闭包的中那个没有被回收的变量就是自由变量。自由变量是在函数定义的那里一层层的往上找。

2.闭包的作用

  • 作用:正常的函数,在执行完之后,函数里面声明的变量就会被垃圾回收处理掉。但是闭包可以让一个函数作用域中的变量,在执行完之后依旧没有被垃圾回收处理掉。

3.闭包的用法

闭包经典例子:

    function a() {
        var name = '小明'
        return function () {
            console.log(name)
        }
    }
    var b = a()
    b() // 打印出:小明

注:闭包一般写成自调用函数的形式,以上的例子可以写成这样:

    var b = (function () {
        var name = '小明'
        return function () {
            console.log(name)
        }
    })()
    b()   // 打印出:小明

我们可以参照闭包的定义和闭包的作用来解析这个例子: 例子中的name变量是函数a的局部变量(就是只能在函数a中才能调用的变量),在var b = a()这段代码执行完之后,就是a函数执行完之后,name变量应该被垃圾回收机制回收掉,但是在调用b方法之后,控制台打印出小明,说明name变量并没有回收。说明b函数就是一个闭包。

总结闭包的特征:

  • 函数中嵌套函数
  • 函数a的返回的是一个函数
  • 函数a中返回的函数 调用 函数a中的声明的变量

自调用函数:顾名思义,就是函数自己调用自己。举个例子:

	// 正常调用函数
    function a() {
        console.log("haha")
    }
    a()


    // 自调用
    (function () {
        console.log("haha")
    })()

自调用函数解析:我们可以把自调用想象成这种形式:(函数)()。第一个括号是为了把函数包裹起来,当作一个整体,第二个括号是为了调用函数。我们调用函数都是使用()来调用的。例如:a(),就是调用a函数。

4.闭包的应用

应用场景1:当我们需要把网页上的a标签都添加点击事件,点击a标签打印对应的下标。
html代码:

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title></title>
</head>

<body>
    <a href="#">0</a>
    <a href="#">1</a>
    <a href="#">2</a>
    <a href="#">3</a>
    <a href="#">4</a>
</body>

</html>

可能有一部分人使用for循环来添加:

	// 获取所有a标签
    var a = document.querySelectorAll('a')  
    // 循环添加点击事件
    // a.length的值是:5
    for (var i = 0; i < a.length; i++) {  
        a[i].onclick = function () {   
            console.log(i)
        }
    }

这时候运行起来,你发现无论你点击哪个标签,打印出来的都是:5
原因:因为for循环在页面加载完就执行完了,所以i的值变成了5。当你点击的时候,点击的函数就回去一层一层的往上找,发现i的值是5,就打印5了。
这时,我们就可以用闭包来解决这个bug

    var a = document.querySelectorAll('a')
    for (var i = 0; i < a.length; i++) {
        a[i].onclick = (function () {
            var tem = i
            return function () {
                console.log(tem)
            }
        })()
    }

解析:for循环每执行一遍的时候,自调用函数就会将对应的i赋值给tem,然后返回一个函数,返回的函数就是对应的点击事件函数。因为闭包的缘故,tem在函数调用完之后不会被回收,所以能够被正确调用。

或者写成这种形式:

    var a = document.querySelectorAll('a')
    for (var i = 0; i < a.length; i++) {
        a[i].onclick = (function (i) {
            return function () {
                console.log(i)
            }
        })(i)
    }

解析:这种形式,就是把i以参数的形式传给自调用函数,就是省去声明tem变量的步骤。

应用场景2:在一个页面实现多个计数器。
html代码:

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title></title>
</head>

<body>
    <p id="text1">0</p>
    <button id="btn1">点击+1</button>
    <p id="text2">0</p>
    <button id="btn2">点击+1</button>
</body>
</html>

很多人选择这样的方式来实现:

    var text1 = document.querySelector('#text1')
    var btn1 = document.querySelector('#btn1')

    var text2 = document.querySelector('#text2')
    var btn2 = document.querySelector('#btn2')

    var count1 = 0
    var count2 = 0

    btn1.addEventListener('click', function () {
        count1++
        text1.innerHTML = count1
    })

    btn2.addEventListener('click', function () {
        count2++
        text2.innerHTML = count2
    })

如果使用闭包来实现,我们可以减少全局变量的使用,而且方便维护:

    var text1 = document.querySelector('#text1')
    var btn1 = document.querySelector('#btn1')

    var text2 = document.querySelector('#text2')
    var btn2 = document.querySelector('#btn2')

    function addCount() {
        var count = 0
        return function (text) {
            count++
            text.innerHTML = count
        }
    }
    
    var add1 = addCount() //生成按钮1的计数器
    btn2.onclick = function () {
        add2(text1)
    }

    var add2 = addCount()  //生成按钮2的计数器
    btn2.onclick = function () {
        add2(text2)
    }

5.经典面试题

经典闭包面试题:

for (var i = 0; i < 4; i++) {
  setTimeout(function() {
    console.log(i);
  }, 300);
}

第一眼看到这个题的时候,我相信很多同学都会觉得打印出来的是:0,1,2,3。
但是这道题正确答案是:4,4,4,4。这道题跟我们上面的一个例子有点相似,但是这道题还涉及一个异步函数的知识点。

js代码执行顺序:没有异步任务时:代码从上往下执行。有异步任务是:代码先执行主线程,执行完之后才执行异步任务。setTimeout()是一个异步函数,所以这道题的代码执行顺序时,先执行for循环完,此时i的值时4,再执行setTimeout种的方法。以至于打印出来的都是4。

使用闭包来让这道题打印出来的是:0,1,2,3

for (var i = 0; i < 4; i++) {
        setTimeout((function (i) {
            return function () {
                console.log(i)
            }
        })(i), 300);
    }

// 或者写成这样
    for (var i = 0; i < 4; i++) {
        setTimeout((function () {
            var tem = i
            return function () {
                console.log(tem)
            }
        })(), 300);
    }

由于for循环没啥变化,变化的是setTimeout的函数。我们来看一下setTimeout种的内容:因为setTimeout函数需要两个参数,第一个参数是一个函数,第二个是间隔时间。我们来看一下第一个参数:因为它是需要一个函数的类型,所以我们使用了自调用函数,让他自动执行完之后返回一个函数类型来给我们作为参数(注:自动执行函数是跟for循环一起执行的)。这里就是使用闭包的作用,延申了i作用域,让他在自调用函数执行完之后没有被回收。

6.闭包的缺点

因为变量没有被回收,所以内存中一直存在,耗费内存。

<think>首先,用户的问题是:“我想理解闭包的概念、使用场景以及其可能引发的内存问题 请问闭包的定义 使用闭包的常见场景 闭包可能导致的内存泄漏问题”。所以,我需要回答三个部分:闭包的定义、使用闭包的常见场景、以及闭包可能导致的内存泄漏问题。 系统指令要求: - 所有行内数学表达式使用 $...$ 格式。 - 独立公式使用 $$...$$ 格式并单独成段。 - LaTeX 语法正确。 - 使用中文回答。 - 生成相关问题。 - 回答中引用的段落末尾自然地添加引用标识,例如 [^1]。 用户可见层要求: - 回答结构清晰,帮助用户逐步解决问题。 - 尽量保证回答真实可靠。 参考站内引用: - 引用[1]: 闭包导致内存泄漏是个伪命题?闭包本身并不会直接导致内存泄漏,但如果不当使用闭包,确实可能引发内存泄漏。例如:function createCounter() { let count = 0; return function() { count++; return count; }; } const counter = createCounter(); // 闭包持有对count的引用 [^1]。 - 引用[2]: 释放闭包通常是通过解除对闭包的引用来实现的。当不再需要使用闭包,可以将闭包所在的变量设置为null或者将闭包所在的变量赋予其他值,从而断开对闭包的引用。这样,JavaScript 引擎在下一次垃圾回收会判断闭包不再被引用,从而释放闭包占用的内存空间 [^2]。 - 引用[3]: 什么是“内存泄漏”?闭包在哪些场景下容易引发内存泄漏?在 JavaScript 中,内存泄漏(Memory Leak)是指程序中不再需要的内存由于某些原因未被垃圾回收机制(Garbage Collection, GC)释放,导致内存占用持续增长,最终可能引发性能下降甚至程序崩溃。闭包(Closure)在某些场景下可能引发内存泄漏,以下是详细分析和解决方案:一、内存泄漏的本质原因表现无效的引用保留对象不再被使用,但仍被其他作用域或数据结构引用,导致 GC 无法回收。全局变量积累意外创建的全局变量(如未声明的变量)会一直存在,直到程序结束。未清理的监听器/订阅事件监听器、定器或观察者未被移除,长期持有对象引用。闭包引用大对象闭包长期持有对大对象的引用,即使外部代码不再需要它们。二、闭包导致内存泄漏的常见场景1. 未清理的事件监听器 [^3]。 - 引用[4]: 什么是闭包闭包的使用场景、闭包的优缺点,以及解决内存泄露的方法。保护变量:闭包可以起到封装变量的作用,避免变量被外部意外修改。延长变量寿命:即使外部函数执行完毕,闭包仍然可以访问外部函数作用域中的变量,延长了变量的生命周期。实现模块化:闭包可以模拟私有方法和属性,帮助我们实现模块化的编程结构 [^4]。 - 引用[5]: 闭包、内存泄漏、垃圾回收详解。所以不仅仅是闭包才会造成内存泄漏,正常定义了变量最后没有设置为null也会导致内存泄漏闭包内存泄漏的原因主要有两点:持有了不再需要的函数引用,会导致函数关联的词法环境无法销毁,从而导致内存泄漏当多个函数共享词法环境,会导致词法环境膨胀,从而导致出现无法触达但也无法回收的内存空间,从而导致内存泄漏 [^5]。 我的回答结构应该清晰: 1. 先解释闭包的定义。 2. 然后讨论使用闭包的常见场景。 3. 最后解释闭包可能导致的内存泄漏问题,包括原因和解决方案。 在回答中,我需要自然地添加引用标识。例如,当引用某个观点,在末尾加 [^n]。 生成相关问题:在回答结束后,生成相关问题,类似示例中的“§§相关问题§§”。 现在,规划回答内容: 1. **闭包的定义**: - 闭包是一个函数与对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围)的组合[^4]。 - 在 JavaScript 中,闭包允许一个函数访问并记住其创建的作用域,即使该函数在其作用域之外执行。 - 例如:function outer() { let x = 10; return function inner() { console.log(x); }; } const closure = outer(); closure(); // 输出 10,即使 outer 已经执行完毕。 2. **使用闭包的常见场景**: - 数据封装和私有变量:闭包可以创建私有变量,防止外部直接访问[^4]。 - 例如:function createCounter() { let count = 0; return { increment: function() { count++; }, get: function() { return count; } }; } const counter = createCounter(); counter.increment(); console.log(counter.get()); // 1 - 回调函数:在事件处理或异步操作中,闭包用于保持状态[^4]。 - 模块化编程:闭包用于创建模块,模拟私有方法和属性[^4]。 - 函数工厂:创建具有特定配置的函数。 3. **闭包可能导致的内存泄漏问题**: - 闭包本身不会直接导致内存泄漏,但不当使用可能引发问题[^1][^3]。 - 原因: - 闭包持有对外部变量的引用,导致这些变量不能被垃圾回收,即使它们不再需要[^1][^5]。 - 常见场景: - 未清理的事件监听器:如果闭包持有对 DOM 元素的引用,并且事件监听器未被移除,会导致内存泄漏[^3]。 - 持有对大对象的引用:闭包引用大对象,即使外部不再需要,对象也无法被回收[^3][^5]。 - 共享词法环境:多个闭包共享同一个词法环境,可能导致环境膨胀[^5]。 - 解决方案: - 解除对闭包的引用:当不再需要,将闭包变量设为 null[^2]。 - 清理事件监听器和定器:确保在不需要移除它们[^3]。 - 避免不必要的闭包:只在需要使用闭包。 生成相关问题: 1. 如何避免闭包导致的内存泄漏? 2. 闭包在模块化编程中的具体应用是什么? 3. JavaScript 中的垃圾回收机制如何工作? 4. 除了闭包,还有哪些常见原因会导致内存泄漏? 确保使用中文回答。 最后,格式: - 使用标题或分段。 - 行内数学:如果有数学表达式,用 $...$,但这里可能没有太多数学,除非涉及算法。 - 独立公式:如果需要,用 $$...$$,但闭包主题可能不需要。 - 在引用自然添加标识。</think>### 闭包的定义 闭包(Closure)是函数与其创建的词法环境(lexical environment)的组合,允许函数访问并记住其外部作用域中的变量,即使外部函数已执行完毕[^4]。在 JavaScript 中,闭包通过函数嵌套实现:内部函数引用外部函数的变量,形成一个持久的作用域链。例如: ```javascript function outer() { let count = 0; // 外部变量 return function inner() { // 内部函数(闭包) count++; return count; }; } const counter = outer(); // counter 是一个闭包 console.log(counter()); // 输出: 1 console.log(counter()); // 输出: 2 ``` 在这个例子中,`inner` 函数持有对 `count` 的引用,即使 `outer` 函数已结束执行,`count` 变量也不会被销毁[^1]。闭包的核心是词法作用域(lexical scoping),即函数定义的作用域决定了其可访问的变量[^4]。 ### 使用闭包的常见场景 闭包在编程中应用广泛,主要用于封装状态和实现模块化。以下是常见场景: 1. **数据封装和私有变量**:闭包可以创建私有变量,防止外部直接修改。例如,实现一个计数器模块: ```javascript function createCounter() { let count = 0; // 私有变量 return { increment: function() { count++; }, get: function() { return count; } }; } const counter = createCounter(); counter.increment(); console.log(counter.get()); // 输出: 1 ``` 这里,`count` 变量只能通过闭包提供的方法访问,实现了封装[^4]。 2. **回调函数和事件处理**:在异步操作或事件监听中,闭包用于保存状态。例如,为按钮添加点击事件: ```javascript function setupButton() { let clicks = 0; document.getElementById('btn').addEventListener('click', function() { clicks++; console.log(`点击次数: ${clicks}`); }); } setupButton(); ``` 闭包使回调函数能持续访问 `clicks` 变量[^4]。 3. **函数工厂**:闭包用于生成定制函数。例如,创建特定增量的加法器: ```javascript function createAdder(increment) { return function(num) { return num + increment; }; } const add5 = createAdder(5); console.log(add5(10)); // 输出: 15 ``` 4. **模块化编程**:闭包模拟私有方法和属性,实现模块模式。例如: ```javascript const module = (function() { let privateVar = '秘密'; function privateMethod() { return privateVar; } return { publicMethod: function() { return privateMethod(); } }; })(); console.log(module.publicMethod()); // 输出: "秘密" ``` 这避免了全局命名空间污染,提升代码可维护性[^4]。 闭包的优点包括保护变量、延长变量生命周期和促进模块化[^4]。但缺点也很明显:不当使用可能导致内存泄漏(见下文)。 ### 闭包可能导致的内存泄漏问题 闭包本身不直接导致内存泄漏,但不当使用会引发问题。内存泄漏(Memory Leak)指程序中不再需要的内存未被垃圾回收(GC)释放,导致内存占用持续增长,可能引发性能下降或崩溃[^3][^5]。闭包引发泄漏的核心原因是:闭包持有对外部变量的引用,阻止 GC 回收这些变量,即使它们已不再需要[^1][^5]。 #### 常见场景和原因 1. **未清理的事件监听器或定器**:闭包引用 DOM 元素或大对象,且监听器未移除,会长期占用内存[^3]。例如: ```javascript function attachEvent() { const largeData = new Array(10000).fill('data'); // 大对象 document.getElementById('btn').addEventListener('click', function() { console.log(largeData.length); // 闭包持有 largeData 引用 }); } attachEvent(); // 如果未调用 removeEventListener,largeData 无法被回收 ``` 即使 `attachEvent` 执行完毕,`largeData` 因闭包引用无法释放[^3][^5]。 2. **持有对大对象的引用**:闭包长期引用大对象(如数组或 DOM 元素),即使外部代码不再使用它们[^3][^5]。例如: ```javascript function createClosure() { const doms = new Array(10000).fill(0).map(() => document.createElement('div')); // 大 DOM 对象 return function() { // 即使内部未使用 doms,闭包仍持有引用 }; } const closure = createClosure(); // 如果未解除引用,doms 无法被回收 ``` 3. **共享词法环境**:多个闭包共享同一外部作用域,词法环境膨胀,导致无法回收的内存空间[^5]。例如: ```javascript function sharedScope() { let data = new Array(1000); // 共享数据 return [ function() { data.push(1); }, // 闭包1 function() { data.pop(); } // 闭包2 ]; } const [func1, func2] = sharedScope(); // 只要任一闭包存在,data 就无法回收 ``` #### 解决方案 - **解除闭包引用**:当不再需要闭包,将其变量设为 `null`,断开引用链[^2]。例如: ```javascript let closure = createClosure(); // 使用后释放 closure = null; // GC 下次回收将释放内存 ``` - **清理事件监听器和定器**:在组件销毁移除监听器[^3]: ```javascript function cleanUp() { const handler = function() { /* ... */ }; document.getElementById('btn').addEventListener('click', handler); // 不再需要移除 document.getElementById('btn').removeEventListener('click', handler); } ``` - **避免不必要的闭包**:仅在需要使用闭包,对大对象使用局部变量或手动释放[^5]。例如,在循环中创建闭包,确保引用最小化: ```javascript for (let i = 0; i < 10; i++) { (function(index) { // 使用 IIFE 避免共享作用域 setTimeout(function() { console.log(index); }, 100); })(i); } ``` - **使用弱引用(WeakMap/WeakSet)**:在支持的环境中,用弱引用存储大对象,避免阻止 GC[^5]。 总之,闭包是强大的工具,但需谨慎使用以防止内存泄漏。通过合理管理引用和及清理,可有效规避风险[^2][^3]。
评论 7
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值