详解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 函数的闭包。

用一句话概括就是

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

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

作者:大眼睛图图

%s 个评论

要回复文章请先登录注册