JS里最难的知识点是闭包?(闭包详解)

1,591 阅读6分钟

什么是闭包?

根据JS词法作用域规则,内部函数总是能访问外部函数中的变量,当通过调用一个外部函数返回的一个内部函数后,即使外部函数执行已经结束,但是内部函数引用了外部函数变量,所以这些变量就需要被保存在内存中,我们把这些变量的集合叫做闭包。学习闭包我们可能会涉及到一些其他的知识点,在解释闭包时我们会一一了解到。

如何产生闭包

闭包的产生,有几个鲜明的特征,它总是在内部函数调用其词法作用域全局作用域内的变量,存在内部函数,也就是说这里有函数的嵌套,为了使闭包生效,外部函数通常会返回内部函数。 这里有我上一篇写的作用域内容,有不理解的小伙伴可以去看看哦。 一看就懂的JavaScript预处理编译和作用域 - 掘金 (juejin.cn)

function foo(){
    var a = 100
    function bar(){
        console.log(a);
    }
    return bar;
}
const baz = foo()
baz() //100

当外部函数执行完毕后,通过返回的内部函数的引用,其作用域链上的外部变量仍然保持活跃状态,不被垃圾回收机制回收。

这里的词法作用域和作用域链对于理解闭包有很好的作用,所以希望大家可以先理解这两部分知识。

作用域链:

那我们用下面的例子来看看什么是作用域链吧,大家可以先做一下这个例子,你的打印结果是什么呢?? image.png 打印的是200,你做对了吗?相信大家都能做对哦,我们还是讲讲它是为什么吧。 其实这都是作用域链在作妖。

作用域链

  1. 函数在执行前发生预编译会创建执行上下文对象,
  2. 变量环境中有一个内定的outer属性用于指明该函数外层作用域是谁
  3. outer的指向是根据词法作用域来定的

首先我们分析一下该代码的作用域执行上下文,如下图所示,函数a、b都定义在全局作用域内,所以在全局执行上下文的变量环境内有 var s 、function a、function b,还有一个最重要的指向作用域功能的outer"指针",在讨论作用域链时提到的“outer”,实际上是指向外层作用域的逻辑连接,而非计算机科学中指针所指的内存地址,它更应该被理解为一种抽象的、逻辑上的包含关系,指示引擎如何在不同的作用域间查找变量。 image.png 而outer指向的是外层作用域,所以就像下面图片一样的指向。

image.png 因为执行的顺序都是从上往下走的,这个图叫调用栈。在执行到console.log(..)函数的时候,要去寻找a变量的值,先在调用该函数的函数作用域的词法环境内寻找,然后就是找变量环境,如果都没找到就会沿着outer的指向去寻找。

总结就是:JS引擎在查找变量是会先在函数中查找,找不到就会根据outer的指向去到外层作用域中查找,层层往上,这种查找的关系链就是作用域链

这样我们就很清楚,在函数b执行到console.log(..)函数的时找不到变量s,这也就会去找outer指向他的词法作用域 -> 全局作用域,所以打印200。

学完了作用域链的知识,让我们再回到闭包的例子上看。

function foo(){
    var name = '小李'
    function bar(){
        console.log(count,age)
    }
    var count = 1
    var age = 18
    return bar
}
var age = 20
const baz = foo()
baz() // 1  18

在上面的示例代码中,foo函数定义了name,count,age 等属性,而其内部的bar函数则调用了foo函数的变量(count和age),因此JavaScript引擎会保留foo的作用域链不被垃圾回收机制回收,且bar()函数可以访问函数foo的执行上下文的变量,在foo函数执行结束后,其函数上下文会被销毁,但会保留部分变量不被销毁。这些不被销毁的变量的集合叫做闭包。

闭包有什么作用

  1. 实现共有变量(适合企业模块开发)

闭包可以帮助实现模块间的私有变量和共享变量,提高代码的封装性和模块化程度。例如,在JavaScript中,可以通过立即执行的函数表达式(IIFE, Immediately Invoked Function Expression)创建一个闭包,用来封装一些只在模块内部使用的变量和方法,同时暴露需要公开的接口给其他模块。

Javascript
1(function() {
2    var privateVar = "我是私有变量";
3    
4    function privateMethod() {
5        console.log(privateVar);
6    }
7    
8    window.myModule = {
9        publicMethod: function() {
10            privateMethod(); // 内部函数可以访问私有变量
11        }
12    };
13})();
14myModule.publicMethod(); // 输出:"我是私有变量"
  1. 用闭包做缓存

闭包可以用来保存计算结果,避免重复计算,达到缓存的效果。

var arr = []
for(var i = 0; i < 10; i++){
   
    (function foo(j){
        // var j = i;
        arr[j] = function(){
            console.log(j);
        }
       })(i)
}
arr.forEach(function(item){
    item()
})

这里还是通过立即执行的函数表达式去创建十个闭包记录i的值,让每一次调用的时候都会拿到准确的i值。

闭包的缺点

  1. 内存泄漏

闭包可能导致内存泄漏,尤其是当它引用了大量数据或长时间维持对外部作用域变量的引用,阻止了垃圾回收机制回收不再使用的内存空间。 因为每次清理函数的执行上下文时,有被内部函数调用的变量,导致还留有一些变量在内存中,这个就是闭包,当数据量太大的时候,闭包较多,而调用栈的容量有限,大量的闭包导致栈满或使得调用栈的可用内存变小了,这就是内存泄漏。

闭包导致内存泄漏的解决方法:

手动释放闭包的引用:在闭包不再需要访问外部变量时,显式地将闭包或者外部变量设置为null。这样可以断开闭包对外部作用域变量的引用,使得垃圾回收机制能够回收这些变量占用的内存。

事件处理和定时器清理:确保为所有的事件监听器和定时器设置清除机制,避免它们长时间持有闭包和相关的外部变量。