10 作用域链和闭包

150 阅读6分钟

如何通过词法环境和变量环境来查找变量,这其中就涉及到作用域链作用域链的概念.

理解作用域链是理解闭包的基础.

什么是作用域链?

function foo(){
    console.log(test);
}

function foo2() {
    var test="1";
    foo();
}

var test="2";
foo2();

// 很显然结果是2 为什么是2?分析下过程

/*
执行上下文为

foo上下文   变量环境:可执行代码 console.log(test) 打印的到底是谁?
foo2上下文  变量环境:test="1"
全局上下文   变量环境:test="2"

从代码中可以看到全局作用域和foo2作用域都有定义test 
那foo 到底找到的是哪个test?
很多人第一反应是延调用栈去找,这是不对的!。
先foo->foo2->全局 这是不对的!

*/

作用域链

关于作用域链,很多人会感觉费解,但如果你理解了调用栈、执行上下文、词法环境、变量环境等概念,那么你理解起来作用域链也会很容易。

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

当一段代码使用了一个变量时,JavaScript引擎首先会在“当前的执行上下文”中查找该变量比如上面那段代码在查找test变量时 没找到就会使用变量环境指向的outer继续查找

foo 的outer指向 全局执行上下文 foo2的outer指向 全局执行上下文 全局 的outer指向 null

foo、foo2 outer 都指向全局执行上下文,变量也会顺着去查找,我们称这个查找的路径为作用域链。

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

词法作用域

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

截屏2023-05-10 上午9.01.22.png

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

回答上面的问题 为什么foo的outer不是foo2? 因为词法作用域的存在,foo foo2都是定义在全局所以在解析的时候就决定了,他们的outer是全局作用域。AST SCOPE阶段

词法作用域是代码阶段就决定好的,和函数是怎么调用的没有关系数是怎么调用的没有

块级作用域中的变量查找


function bar() {  
var myName = "a"  
let test1 = 100  
if (1) {  
let myName = "b"  
console.log(test)  
}  
}  
function foo() {  
var myName = "c"  
let test = 2  
{  
let test = 3  
bar()  
}  
}  
var myName = "d"  
let myAge = 10  
let test = 1  
foo()

test 的查找路径 bar的if 块级词法环境->bar的词法环境->bar的变量环境outer->全局执行上下文词法环境->全局执行上下文的变量环境。

查找先词法在变量在outer

什么是闭包? 解了变量环境、词法环境和作用域链等概念,那接下来你再理解什么是JavaScript中的闭包就容易多了

function foo() {  
    var myName = "a"  
    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("b")
bar.getName()
console.log(bar.getName())

根据词法作用域的规则,内部函数getName和setName总是可以访问它们的外部函数foo中的变量,所以当innerBar对象返回给全局变量bar时,虽然foo函数已经执行结束,但是getName和setName函数依然可以使用foo函数中的变量myName和test1。

当foo执行结束的时候,myName、test1,会被添加到closure中来,实际上这个地方会在预计解析的时候就创建了地址空间然后把myName,test1创建在这个空间里面了.

foo函数执行完成之后,其执行上下文从栈顶弹出了,但是由于返回的setName和
getName方法中使用了foo函数内部的变量myName和test1,所以这两个变量依然保存在内存中。这像极了setName和getName方法背的一个专属背包,无论在哪里调用了setName和getName方法,它们都会背着这个foo函数的专属背包之所以是专属专属背包,是因为除了setName和getName函数之外,其他任何地方都是无法访问该背包的,我们就可以把这个背包称为foo函数的闭包闭包

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

setName的作用域链是什么? 当前执行上下文 -> 闭包 -> 全局上下文。

截屏2023-05-10 上午10.42.50.png

闭包是怎么回收的

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

总结 介绍了什么是作用域链,我们把通过作用域查找变量的链条称为作用域链;作用域链是通过词法作用域来确定的,而词法作用域反映了代码的结构。

介绍了在块级作用域中是如何通过作用域链来查找变量的。

作用域链和词法环境介绍了到底什么是闭包

作用域链是已经注定好了,比如即使在foo函数中调用了bar函数,你也无法在bar函数中直接使用foo函数中的变量信息。