闭包?B包!—— 一篇文章带你无痛理解闭包

前言

在一场面试中,面试官问道:什么是闭包?此时你的脑子里浮现出一场csgo的对局,你正守着B包点,对面五个匪提着冲锋枪直接rush B,你吃满了所有道具和枪线,最后你只能开麦大喊:B包!

好了,现在你已经理解闭包了。

哈哈,开个玩笑,让我来强行解毒一下:此时你就是一个外层函数,而你的队友则是内部函数,现在你已经被乱枪打死了(外层函数已经执行完毕了),你也不知道接下来你的队友会如何发挥(外层函数不知道内部函数有没有执行),但你死了并不能进行任何操作(外层函数执行完要被垃圾回收机制回收),所以你只能给队友留下信息:B包!(所以外层函数给内部函数留下的数据的集合就叫做闭包)。

如果你还是没有理解,别急,这只是前菜,接下来开始正式的讲解:

让我们先看一段代码:

js 复制代码
var arr = []
for(var i = 0;i<10;i++){
    arr[i]=function(){
        console.log(i);
    }
}
arr.forEach(function(item){
    item()
})

你知道这段代码会输出什么结果吗?肯定有人会说输出0到9对吧,那让我们运行一下。

可以看到控制台输出的结果为10个10。现在你心中可能有一万个为什么,不要急,耐心看完这篇文章,你就能知道这其中的奥妙。

首先我们来分析一下这段代码,不就是用for循环遍历了一个数组arr,存进去了10个函数嘛,但主要问题不在这,而是再次遍历输出数组时里面所存储的函数执行后会输出个啥,可以看到函数体内容是console.log(i),要输出i,首先就得找到i,而只有在全局中有定义一个i,且经过10次for循环后i现在的值为10,所以就有了上图的结果。

那如果现在我们就要让他输出0到9,该怎么办呢,第一种方法,我们可以把声明变量ivar改成let,再来看看结果

这里涉及到的是块级作用域的知识点,因为let+{}形成块级作用域, for循环10次,形成10个块级作用域,函数执行时去当次形成的作用域找i,于是就输出了0到9。但这不是我们这次主要讲解的知识点,我们要考虑如何用闭包解决这个问题。要理解闭包,我们先来了解一下作用域链。

作用域链

我们再来看一段代码,

js 复制代码
function bar(){
    console.log(a);
}
function foo(){
    var a = 100
    bar()
}
var a = 100
foo()

因为bar定义在全局中,所以输出a时函数会去全局中找a,结果找到a=100,即输出100

可以看到运行结果确实是100,那我们来看个更复杂的

js 复制代码
function bar(){
    var myname ='tom'
    let test1 = 100
    if(1){
        let myname='jerry'
        console.log(test,myname)
    }
}
function foo(){
    var myname = 'mike'
    let test = 2
    {
        let test = 3
        bar()
    }
}
var myname = 'jesper'
let test = 1
foo()

这段代码较为复杂,让我们画图来理解一下

  1. 首先全局的执行上下文先进入调用栈。
  2. 按照函数的执行顺序,然后是foo的全局上下文进入调用栈。
  3. 最后是bar,执行到bar函数时,要输出testname

注意!bar函数寻找变量并不是按普通的从上往下找,而是按照作用域链查找。

  • 作用域链,简单来说,作用域链就是一种查找关系,变量环境中有一个内定的outer属性用于指明该函数的外层作用域是谁。

因为bar函数在全局中创建,所以bar变量环境中的outer指向全局,foo同理。mynamebar函数内部找到,值为jerrybar函数内部找不到变量test,所以按照作用域链去全局中找,得到值为1,所以输出的为1 jerry

垃圾回收机制和闭包

到这里,你应该差不多理解了作用域及作用域链,接下来我们看最后一段代码

js 复制代码
function foo(){
    var name= 'jesper'
    function bar(){
        console.log(count,age)
    }
    var count = 1
    var age = 18
    return bar
}
var age = 20
const baz = foo()
baz()

我们按刚刚的方法画图分析

bar函数要输出countage,但函数内部并没有变量countage,于是沿着作用域链去foo函数中找,找到count=1,age=18

但结果是不是这样呢,可以说是也不是,如是。因为其实调用栈还有一个规则,即垃圾回收机制,就是当一个函数执行完后,他是应该被销毁的,可以看到上面的foo函数returnbar后,明显执行完毕了,理应被销毁,但如果foo被销毁了,他内部函数所需变量怎么办呢,因为内部函数离家出走,外部函数并不知道她还有没有执行,既然生下了她,就得对她负责吧,所以外部函数被销毁时,就会在调用栈留下一个小背包,用来存储内部函数可能会用到的变量,用专业点的词说,就叫闭包。

恭喜你现在就已经理解闭包的形成了。

闭包的优缺点

那闭包又有什么作用和缺点呢

作用:

  • 实现共有变量:闭包可以创建一个共享的变量空间,让多个函数可以访问同一个变量。这对于实现函数之间共享状态非常有用。
  • 做缓存:使用闭包可以实现函数结果的缓存,这样对于相同的输入,可以直接返回之前计算的结果,避免重复计算。
  • 封装模块,防止全局变量污染:闭包可以用来创建独立的模块,这些模块不会污染全局命名空间。

缺点:

  • 内存泄漏:闭包越来越多,调用栈可用空间越来越小

开头的题目

最后我们回到开头的那道题,是不是用闭包也能轻松解决呢。

js 复制代码
for(var i = 0;i<10;i++){
    function foo(){
        var j = i
        arr[j]=function(){
            console.log(j);
        }
    }
    foo()
}

arr.forEach(function(item){
    item()
})

我们只需要在外面用一个函数把储存的函数围起来,这样每次for循环因为外部函数不知道数组内存储的函数有没有被执行,所以被销毁时都会留下一个闭包,存储当时i的值,这样数组在外部调用存储的函数时,是不是每次都能到形成的闭包中找到对应的值。可以看到最后结果确实为0到9.

结语

总之,我们要知道,闭包的产生就是因为JS的两种机制,即作用域链和垃圾回收机制的冲突,内部函数会沿着作用域链查找变量,而外层函数执行完后因垃圾回收机制需要销毁,所以就有了闭包,即外层函数留下内部函数可能会用到的变量的集合就叫做闭包。

相关推荐
敲啊敲952713 分钟前
5.npm包
前端·npm·node.js
贵州晓智信息科技19 分钟前
如何优化求职简历从模板选择到面试准备
面试·职场和发展
CodeClimb23 分钟前
【华为OD-E卷-木板 100分(python、java、c++、js、c)】
java·javascript·c++·python·华为od
咸鱼翻面儿27 分钟前
Javascript异步,这次我真弄懂了!!!
javascript
brrdg_sefg27 分钟前
Rust 在前端基建中的使用
前端·rust·状态模式
m0_748230941 小时前
Rust赋能前端: 纯血前端将 Table 导出 Excel
前端·rust·excel
qq_589568101 小时前
Echarts的高级使用,动画,交互api
前端·javascript·echarts
黑客老陈2 小时前
新手小白如何挖掘cnvd通用漏洞之存储xss漏洞(利用xss钓鱼)
运维·服务器·前端·网络·安全·web3·xss
正小安2 小时前
Vite系列课程 | 11. Vite 配置文件中 CSS 配置(Modules 模块化篇)
前端·vite