详解JavaScript中的作用域链与闭包

 更新时间:2022年11月30日 10:18:51   作者:大眼睛图图  
这篇文章主要为大家详细介绍一下JavaScript中的作用域链与闭包的使用,文中的示例代码讲解详细,对我们学习JavaScript有一定的帮助,需要的可以参考一下

作用域链

首先来看看这段代码:

var a = '喜羊羊';
function A(){
    console.log(a);
    a = '美羊羊';
    function B(){
        console.log(a);
    }
    B();
}
A();

在这里毫无疑问结果肯定是我们想到的先打印喜羊羊,再打印美羊羊。因为作用域链嘛,如果当前层没找到,那么就去当前层的上一级找。

那么再看这道

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

是不是感觉是打印极客邦?如果是的话,那么恭喜你,掉坑里了。(还不赶快爬起来,补一补作用域链的知识)。

为什么打印不是极客邦而是极客时间呢?

既然问题出现在了对作用域链的理解上,那么就再回到作用域链的定义上吧。

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

比如上面那段代码在查找 myName 变量时,如果在当前的变量环境中没有查找到,那么 JavaScript 引擎会继续在 outer 所指向的执行上下文中查找

为了直观理解,你可以看下面这张图:

看到这张图我猜你又纳闷了,为什么bar函数创建的执行上下文中的outer会指向全局??

哈哈哈,这里就要涉及到了词法作用域了

词法作用域

词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符。

这么讲可能不太好理解,你可以看下面这张图:

从图中可以看出,词法作用域就是根据代码的位置来决定的,其中 main 函数包含了 bar 函数,bar 函数中包含了 foo 函数,因为 JavaScript 作用域链是由词法作用域决定的,所以整个词法作用域链的顺序是:foo 函数作用域—>bar 函数作用域—>main 函数作用域—> 全局作用域。

明白了词法作用域,那么我们再回到刚刚的问题。

为什么bar函数创建的执行上下文中的outer会指向全局

这是因为根据词法作用域,而词法作用域又是根据代码的位置,而bar函数代码的位置就是包裹在全局下,而喜羊羊那个例子中的B函数是在A函数的环境下,所以会造成它们的词法作用域链不同,也就导致函数作用域链不同了。

所以我们才有那句话词法作用域是代码编译阶段就决定好的,和函数是怎么调用的没有关系。

也就是只和代码位置有关,和函数直接如何调用没关系

闭包

老生常谈的问题,这次再从一个更深入的角度来理解一下。

看下面这段代码:

function foo() {
    var myName = "极客时间"
    let test1 = 1
    const test2 = 2
    var innerBar = {
        getName:function(){
            console.log(test1)
            return myName
        },
        setName:function(newName){
            myName = newName
        }
    }
    return innerBar
}
var bar = foo()
bar.setName("极客邦")
bar.getName()
console.log(bar.getName())

这段代码乍一看没有什么问题,但是这里有一个细节很多人会忽视。

在foo()执行完将返回值给bar时,这里foo函数会从调用栈中弹出,变量都会被回收。既然变量都被回收了,那么bar.setName()这些调用方法从何而来??

foo执行完后的情况可以参考下图:

从上图可以看出,foo 函数执行完成之后,其执行上下文从栈顶弹出了,但是由于返回的 setNamegetName 方法中使用了 foo 函数内部的变量 myNametest1,所以这两个变量依然保存在内存中。这像极了 setNamegetName 方法背的一个专属背包,无论在哪里调用了 setName getName 方法,它们都会背着这个foo函数的专属背包。

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

好了,现在我们终于可以给闭包一个正式的定义了。在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包 比如外部函数是 foo,那么这些变量的集合就称为 foo 函数的闭包。

用一句话概括就是

能够访问其他函数内部变量的函数,被称为 闭包

(我们理解可以这么理解,但是和面试官说的当然可以把这个例子说一下,这直接上升到了一个理解什么是闭包的新高度了)

到此这篇关于详解JavaScript中的作用域链与闭包的文章就介绍到这了,更多相关JavaScript作用域链 闭包内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

最新评论