谈谈作用域链和闭包

在这里插入图片描述

来自 极客时间 ,李兵老师所写的《浏览器工作原理与实践》,太赞了

理解作用域链是理解闭包的基础,而闭包在JavaScript中几乎无处不在,同时作用域和作用域链还是所有编程语言的基础。所以今天来聊聊什么是作用域链,并通过作用域链来讲讲什么是闭包

首先看一段代码

     function bar () {
        console.log(myName);
       }
       function foo () {
        var myName = "极客邦";
        bar();
        console.log(myName);
       }
       var myName = "极客时间";
       foo()

通过前面的 谈谈调用栈 的学习,想必大家也知道如何通过执行上下文来分析代码的执行流程了。当这段代码执行到bar函数内部时,其调用栈的状态图如下所示:
在这里插入图片描述
从图中可以看出,全局执行上下文和foo函数的执行上下文中都包含变量 myName ,那bar函数里面的myName的值到底该选择哪个呢?

也许你的第一反应时按照调用栈的顺序来查找变量,查找的方式如下:

  1. 先查找栈顶是否存在 myName 变量,但是这里没有,所以接着往下查找 foo 函数中的
    变量。
  2. 在 foo 函数中查找到了 myName 变量,这时候就使用 foo 函数中的 myName。

如果按照这种方式来查找变量,那么最终执行 bar 函数打印出来的结果就应该是“极客邦”。但实际情况并非如此,如果你试着执行上述代码,你会发现打印出来的结果是“极客时间”。为什么会是这种情况呢?要解释清楚这个问题,那么你就需要先搞清楚作用域链了。

作用域链

每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用称为outer

比如上面那段代码在查找myName变量时,如果在当前的变量环境中没有查找到,那么JavaScript引擎会继续在outer所指向的执行上下文中查找。为了直观理解,可以看下面这张图:
带有外部引用的调用栈示意图
从图中可以看出,bar函数和foo函数的outer都是指向全局上下文的,这也就意味着如果在bar函数或者foo函数中使用了外部变量,那么JavaScript引擎会去全局执行上下文中查找。我们把这个查找的链条就称作为作用域链

那你会奇怪了,foo 函数调用的bar 函数,那为什么 bar 函数的外部引用是全局执行上下文,而不是 foo 函数的执行上下文?

要回答这个问题,你还需要知道什么是词法作用域。这是因为在JavaScript执行过程中,其作用域链是由词法作用域决定的。

词法作用域

词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。可能这还不是很理解,可以看这张图:
词法作用域
从图中可以看出,词法作用域是根据代码的位置来决定的,其中mian函数包含了bar函数,bar函数中包含了foo函数,因为JavaScript作用域链是由词法作用域决定的,所以整个词法作用域链的顺序是:foo函数作用域——》bar函数作用域——》main函数作用域——》全局作用域、

我们了解了JavaScript中的作用域链,继续我们之前的问题:在开头那段代码中,foo 函数调用了 bar 函数,那为什么 bar 函数的外部引用是全局执行上下文,而不是 foo 函数的执行上下文?

这是因为根据词法作用域,foo和bar的上级作用域都是全局作用域,所以如果foo或者bar函数使用了一个它们没有定义的变量,那么它们回到全局作用域去查找。也就是说,词法作用域是代码阶段(代码的位置)就决定好的,和函数是怎么调用的没有关系。

块级作用域中的变量查找

前面我们是是通过全局作用域和函数级作用域来分析了作用域,接下看块级作用域中的变量是如何查找的?其实都是一样的思路,我们先看下代码:

    function bar () {
        var myName = "极客世界";
        let test1 = 100;
        if(1) {
            let myName = "Chrome 浏览器";
            console.log(test);
        }
       }
       function foo () {
        var myName = "极客邦";
        let test = 2;
        {
            let text = 3;
            bar();
        }
       }
       var myName = "极客时间";
       let myAge = 10;
       let test = 1;
       foo()

分析:
在这里插入图片描述
解释下这个过程,首先是在bar函数执行上下文中查找,但bar函数的执行上下文没有定义test变量,所以根据词法作用域的规则,下一步就在bar函数的外部作用域中查找,也就是全局作用域。

闭包

先看一段代码来理解什么是闭包:
在这里插入图片描述
上面代码的调用栈如下:
执行到renturn bar时候的调用栈
从上面的代码可以看出,innerBar 是一个对象,包含了 getName 和 setName 的两个方法(通常我们把对象内部的函数称为方法)。你可以看到,这两个方法都是在 foo 函数内部定义的,并且这两个方法内部都使用了 myName 和 test1 两个变量。根据词法作用域的规则,内部函数 getName 和 setName 总是可以访问它们的外部函数 foo 中的变量,所以当 innerBar 对象返回给全局变量bar时,虽然foo函数已经执行结束,但是getName 和 setName 函数依然可以使用foo函数中的变量 myName 和 test1。所以当foo函数执行完成之后,整个调用栈的状态如下图所示:
闭包的产生过程
从上图可以看出,foo 函数执行完成之后,其执行上下文从栈顶弹出了,但是由于返回的setName 和 getName 方法中使用了 foo 函数内部的变量 myName 和 test1,所以这两个变量依然保存在内存中。这像极了 setName 和 getName 方法背的一个专属背包,无论在哪里调用了 setName 和 getName 方法,它们都会背着这个 foo 函数的专属背包。

之所以是专属背包,是因为除了 setName 和 getName 函数之外,其他任何地方都是无法访问该背包的,我们就可以把这个背包称为 foo 函数的闭包。

因此,闭包是一个内部函数总是可以其访问外部函数中声明的变量,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合(内部函数)称为闭包。 比如外部函数是 foo,那么这些变量的集合就称为foo函数的闭包。

闭包是怎么回收的

理解了什么是闭包之后,接下来聊下它是什么时候销毁的。因为闭包使用不正确,会很容易造成内存泄漏的。

如果引用闭包的函数是个全局变量,那么闭包会一直存在直到页面关闭,但这个闭包如果后面都不再使用的话,就会造成内存泄漏。

如果引用闭包的函数是个局部变量,等函数销毁后,在JavaScript引擎执行垃圾回收时,判断闭包这块内容如果已经不再被使用了,那么JavaScript引擎的垃圾回收器就会回收这块内存。

所以在使用闭包的时候,你要尽量注意一个原则:如果该闭包会一直使用,那么它可以作为全局变量而存在;但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它成为一个局部变量。

总结

在这里插入图片描述

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

*neverGiveUp*

你的鼓励是我最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值