面试官问你闭包 你: 闭包?什么是闭包?

206 阅读10分钟

前言

在学js中,有很多小伙伴刚开始学习到闭包这个知识点时可能会感觉有难,有点懵,不好理解,闭包?啥是闭包?它有什么用?今天小编就带大家一起来了解一下闭包这个知识点。

什么是闭包?

在javaScript中,根据词法作用域的规则,内部函数一定能访问外部函数的变量,当内部函数被拿到外部函数之外调用时,即使外部函数执行完毕,但是内部函数对外部函数的变量依然存在引用,那么这些被引用的变量会以一个集合的方式保存下来,这个集合就是闭包。

咱们先来看一个案例让大家认识一下闭包:

function foo() {
    function bar() {
        var age = 18;
        console.log(myName);
    }
    var myName = "Tom";
    return bar
}
var myName="Jerry";
var fn=foo();
fn();

我们一起来逐步分析一下: 首先咱们进行全局的预编译

GO{
myName:undefined
fn:undefined
foo:function foo(){}
}

准备好后入栈开始执行全局的执行上下文:

GO{
myName:undefined  ->jerry
fn:undefined ->foo()
foo:function foo(){}
}

于是开始函数的预编译,也就是foo的预编译和入栈执行foo的执行上下文:

AO{
myName:undefined->jerry
bar:function bar(){}
}

foo执行上下文准备好后开始入栈执行最终会返回bar这个函数体,返回这个函数给fn后,正常来讲foo这个执行上下文是已经进行了自我销毁。fn现在就成了bar这个函数体,这个函数里有age这个属性,于是fn()就开始调用这个函数,这个函数需要打印myName这个变量,myName这个变量在函数体内没有,于是查看outer指针,发现outer指向的是foo这个函数执行上下文,但问题来了,这个foo执行上下文在foo()赋值给fn时就已经进行了销毁,所以打印结果时报错才对。可是这道题的运行结果居然是

Snipaste_2024-05-07_00-13-32.png

这是为什么呢,原来我们的js执行引擎在看到函数体内还有函数的时候,在执行完外部函数进行销毁的瞬间会丢出一个包,就是我们所说的闭包,这个包就用来存放内部函数需要用到的变量,这里就是myName,丢出包也就是执行完foo,在此之后执行fn,也就是bar这个函数体,这个时候barout指针指向的位置由原来的foo变为了foo丢下的闭包,于是在丢下的闭包寻找myName找到其值为Tom,最后打印输出Tom。
整个过程有点绕,这里我画个图来帮助大家理解一下:

938D681D-DF4E-4135-AE8E-4CCB844CC79E.png 有的小伙伴这里可能会有个疑问,如果没有var myName = "Tom"这条语句,也就是最后丢下的闭包中没有myName这个变量,那怎么办呢?会报错吗?还记得咱们之前讲过的作用域链嘛,其实这里在这个闭包中依然会有一个outer指针,这个指针指向的词法作用域和原来foo指向的词法作用域一样,都是指向全局作用域的,所以如果没有var myName = "Tom"这条语句,就会顺着outer指针指向下一作用域就是全局作用域,于是找到myName=jerry,就会打印这个。

再来看一道题目:来看看这里会输出什么?

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

这个代码看起来好像有点不友好,但是仔细阅读我们还是可以理解的。

这段代码意思就是给出一个数组,里面放10个函数,分别打印对应的i值。然后后面就是调用每个函数。按道理应该是打印0123456789。可是,真的是这样吗?

Snipaste_2024-05-07_00-44-09.png

我们运行后发现结果为10个10!???这是为什魔??
其实var就是原因,如果我们改成let,那么最后输出就是0123456789。这又是为什么,我们来一起逐步分析一下。

第一个for循环仅仅是给了10个函数声明,至于函数体里面我们先不用管,等到调用函数的时候我们再来看或者说编译这个函数体,现在执行到第二个for循环,函数名加一个括号就是函数的调用,因此这是在调用arr[0]arr[1]arr[9]这10个函数。我们就先来看第一个,arr[0],里面只有一个执行语句,console.log(i),i不在arr[0]这个函数体内,于是我们要靠outer指针找到i,outer是指向全局作用域的,因为arr[0]声明在全局作用域中,那么i现在等于多少呢,重点到啦!!当我们要开始执行第二个for循环里的函数的时候,就代表第一个for循环里面的语句已经走完了,也就说i这个值已经从0走到了9,最后9还是小于10,i++,因此i最终的值为10!!跳出循环,所以我们的arr[0]打印出的值是10,由于后面所有的函数都是指向全局里的i,并且执行每一个函数的时候,i已经都是10了,所以最后的运行结果就是10个10!

简单的说就是i这个值存在于全局中,等到执行函数的时候,i已经为10了,10个函数都是打印i,所以结果就是10个10。

那为什么把var改成let了就可以正常输出0到9呢? 前面的文章咱们已经知道,let可以和{}结合会形成一个块级作用域,因此会形成10个块级作用域,每个块级作用域都有一个i,因此10个函数声明在这里就是声明在对应的块级作用域中,也就是outer指向了对应的块级作用域,第一个函数找i时,去第一个块级作用域中找,里面的i是0,第二个函数找i时,去第二个块级作用域中找,里面的i是1,如此循环,就是0到9了。每个function声明在每次let i与花括号形成的作用域中,因此每次的i都是不同的值。 i 现在咱们知道这个代码的输出原理了,我们就是希望能够输出0到9,这里的用let声明i是一个方法,除了这个还有什么方法呢? 还有一种方法就是用闭包的思想,我们现在需要的是一个不改变var的情况下正常输出0到9这些数,既然要用闭包,我们就需要一个双层嵌套函数的结构,既然解决的是i没有赋值的问题,我们可以把i丢到闭包中,然后让console.log(i)指向这个i,就可以了。因此我们需要在第一个for循环中新增一个外层函数,让i赋值进去,我们就需要一个形参来接收。

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

for(var k = 0; k < arr.length; k++){
    arr[k]()
}

刚开始看可能会有点不好懂,因为这里用到了自执行函数。为了方便大家理解,我先解释下自执行函数。其实很好理解,我们声明一个foo函数是写成function foo(){},调用这个函数的时候foo()即可,这个foo就是一个函数体,因此我们完全可以把foo换成一个函数体,当然这里需要再加个(),于是这就是一个自执行函数

function foo(){
}
foo()
等同于
(function foo(){})()

这个for循环中的外层函数a()声明完后立即执行,并且还把i传了进去,里面的内层函数结构咋样咱们先不管,因为里面并没有return和其他的执行语句,只是把一个函数体赋值给了arr[0](i=0为例),这里需要注意,当a()函数执行完毕的时候,这个a()的执行上下文就会销毁掉,a的执行上下文里面只有i这个参数,并且i = 0,在销毁的时候它需要留一下里面的内层函数是否会用到自己的参数,发现内层函数确实需要,于是arr[0]里面的i就是指向了a[0]留下的闭包,如此循环,每个arr都有自己对应的a,并且a()销毁后留下对应的闭包给到arr,因此最后执行10个arr内部函数的时候就是打印0-9了。

如果上面的内容你都看完,两道案例你都看懂并且能够理解到位,那么恭喜你,你已经理解了闭包。

总结:

一、在javaScript中,根据词法作用域的规则,内部函数一定能访问外部函数的变量,当内部函数被拿到外部函数之外调用时,即使外部函数执行完毕,但是内部函数对外部函数的变量依然存在引用,那么这些被引用的变量会以一个集合的方式保存下来,这个集合就是闭包。
二、怎么实现闭包 闭包是指一个函数可以访问它定义时所在的词法作用域以及全局作用域中的变量。在JavaScript中,闭包可以通过函数嵌套和变量引用实现。
三、闭包的作用
1.封装私有变量
闭包可以用于封装私有变量,以防止其被外部访问和修改。封装私有变量可以一定程度上防止全局变量污染,使用闭包封装私有变量可以将这些变量限制在函数内部或模块内部,从而减少了全局变量的数量,降低了全局变量被误用或意外修改的风险。
2.做缓存
函数一旦被执行完毕,其内存就会被销毁,而闭包的存在,就可以保有内部环境的作用域。
3.模块化编程(实现共有变量)
闭包还可以用于实现模块化编程。模块化编程是一种将程序拆分成小的、独立的、可重用的模块的编程风格。闭包可以用于封装模块的私有变量和方法,以便防止其被外部访问和修改。
四、闭包的缺点
闭包也存在着一个潜在的问题,由于闭包会引用外部函数的变量,但是这些变量在外部函数执行完毕后没有被释放,那么这些变量会一直存在于内存中,总的内存大小不变,但是可用内存空间变小了。 一旦形成闭包,只有在页面关闭后,闭包占用的内存才会被回收,这就造成了所谓的内存泄漏。
因此我们在使用闭包时需要特别注意内存泄漏的问题,可以用以下两种方法解决内存泄露问题:

1.及时释放闭包:手动调用闭包函数,并将其返回值赋值为null,这样可以让闭包中的变量及时被垃圾回收器回收。
2.使用立即执行函数:在创建闭包时,将需要保留的变量传递给一个立即执行函数,并将这些变量作为参数传递给闭包函数,这样可以保留所需的变量,而不会导致其他变量的内存泄漏。

今天的分享到这里就结束啦,看完这篇文章相信你对闭包应该有了一个更深的理解吧。要是觉得这篇文章对你有帮助的话能点个免费的赞赞嘛,感谢感谢!