彻底搞懂闭包

本文深入解析JavaScript中的闭包概念与执行上下文的工作原理,通过实例讲解如何利用作用域链实现变量的持久访问。

闭包

首先来看一个问题

function books() {
  var book = '书包里有一本书'
}
console.log(book)

这个执行显然是出错的,为什么呢,这里就牵扯到了闭包,下面会用执行上下文来解释

执行上下文

每当运行代码时就会生成执行上下文,决定了代码的作用域,js的执行环境分为三种:

  • 全局环境
  • 函数环境
  • eval环境(慎用)

当初次运行代码,会进入全局环境

例如:

function a(){
    b()
    function b(){
        c()
        function c(){
            console.log('c')
        }
    }
}
a()

在上述代码中首先会进入全局环境,然后遇到a()时会进入函数a执行上下文,再然后遇到b(),则会进入函数b()的函数b执行上下文,最后遇到c(),进入函数c的执行上下文,这个过程是一个入栈的过程。这就像把薯片一片一片放进薯片罐,放的第一片在最底下,之后放的会叠在上面,拿的时候从最上面开始拿。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bei95e7a-1597033028120)(C:\Users\93221\AppData\Roaming\Typora\typora-user-images\image-20200810102444337.png)]

当c函数执行完了以后会从函数c执行上下文开始出栈,然后一次是b、a、全局,然后代码就可以继续往下执行了,要注意的是js是单线程的形式

这样就可以解释第一个例子,第一个例子book显示未定义的原因在于没有执行函数books,也就是说没有进入函数books的作用域,当然就没有定义book这个变量了呀。

执行上下文的两大步骤

创建阶段

当我们调用了函数但还没有执行的时候,会创建:

  • 作用域链(当前变量对象+所有父级变量对象)
  • 变量对象(参数、变量、函数声明)
  • this指针

执行阶段

当进入了执行阶段,就会进行具体的操作,比如变量赋值、函数引用等

下面就来解释一下,什么是作用域链和闭包

作用域链

下面的解释以段代码为例:

function books(){
  var book='书包里有书'
  return function(){
    console.log(book)
  }
}
var bag=books()
bag()

按照之前说的执行上下文,会有一下过程

  • 1、进入全局执行上下文

全局执行上下文=[作用域链:[全局变量对象],[变量对象:book,bag]]

这就像把第一片全局变量对象的原味薯片放进薯片罐,其余的变量对象跟着创建,有books指向函数books,还有变量bag指向books函数,之后执行bag函数,因为它是指向books的所以会进入books执行上下文

  • 2、books执行上下文

books执行上下文=[作用域链:[books变量对象+全局变量对象],[变量对象book]]

当前作用域链肯定会是自己作用域的变量对象,然后再放之前的,例子中就是全局变量对象。这就像在原本有原味薯片的薯片罐里放一片烧烤味的原味薯片,然后我们就会遇到匿名函数return function(){},这时候我们就为这个匿名函数创建执行上写文,和作用域链

  • 3、匿名函数执行上下文

按道理说执行上下文应该如下所示:

匿名函数执行上下文=[作用域链:[匿名函数变量对象+books变量对象+全局变量对象],[变量对象:]]

但是在这个匿名函数里,这个函数没有自己的变量对象,所以它的执行上下文和books的执行上下文是一致的。也就是说又放了一片烧烤味的薯片进薯片罐

最顶层的薯片包含着之前薯片的所有味道,只要尝试最上面的薯片就可以知道底层薯片的味道,而且最顶层的薯片最新鲜,作用域链也一层链着一层,每新建一层都会包含前一层,最上层的薯片是最优先。

那么现在我们就可以吃薯片了,例子中我们要输出book这个变量,我们就在作用域中进行寻找,因为匿名函数变量对象没有实质内容,所以就找下一层,也就是books变量对象,books函数中有book这个变量,那么就可以输出了

秒杀一半程序员的经典题:

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

js是单线程的,如果每次都要执行完一件事再执行下一个,那么会很慢,所以就出现了任务队列。setTimeout函数中就算时间数为0也不会放入执行栈,而是会放进任务队列,等到执行栈执行完,才会根据时间间隔来执行任务队列中的代码。

所以在上述例子中会先在任务队列中

任务队列:

[外链图片转存在这里插入图片描述

这里有计算结果,就像有五片烧烤味薯片在工厂中加工还没有出炉,在执行栈中有全局执行上下文,也就是还有打印输出i的任务,for循环中没有其他任务,所以会先循环5遍,然后打印一个5

当打印完成后,执行栈就没有别的任务了,那么就会执行任务队列中的任务,这5个i++就会开始按照间隔时间从小到大依次开始执行,因此会一次打印,5,6,7,8,9,也就是5片烧烤味的薯片就可以放入薯片罐。

这里既是匿名函数是浏览器单独进行处理的,但是作用域链依然是不变的,依旧可以访问到变量i,这就是闭包的原理,就算这些是烧烤味薯片,但是这依旧是在原味的基础上制作的。

始执行,因此会一次打印,5,6,7,8,9,也就是5片烧烤味的薯片就可以放入薯片罐。

这里既是匿名函数是浏览器单独进行处理的,但是作用域链依然是不变的,依旧可以访问到变量i,这就是闭包的原理,就算这些是烧烤味薯片,但是这依旧是在原味的基础上制作的。

像这样把函数作为值,放入队列,然后规定时刻进行回调执行的就是创建了一个闭包,也就是常说的回调函数。因为作用域链的关系,哪怕是回调函数内部一样可以访问作用域链的里面的变量对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值