详解 JS 作用域链(小白必看)

365 阅读4分钟

前言

作为js必须搞懂的基本原理之一,作用域链常常以其抽象的概念,让人头大,接下来,我将以图文结合的方式,为大家详解js的作用域究竟为何物 !

1.作用域

要理解作用域链,首先要理解,何为作用域,我们都知道,变量是有范围的,定义在全局的变量叫全局变量,作用域是全局,定义在函数体内部或者在 {} 内的块级代码中的变量,则可称为局部变量,作用域是它所在的花括号,例如:

   var a = 100
       function b(){
       var c =200
       console.log(c)
   }
   b()

a的作用域是全局,c的作用域是函数b内

2.执行期上下文对象

编译器是自上而下逐行执行,先编译全局,遇到函数执行时,再编译函数体

编译器在编译的时候,每处在一个作用域,便会创建一个AO(activation object)对象(又称为执行期上下文的内部对象),然后将遇到的变量当作AO对象的属性,插入AO对象中,所以上述变量a,也是 "AO.a",在全局时时,还会创建一个GO(global object)对象,若编译器执行到了函数b,遇到了函数b作用域,便创建bAO对象,在b执行完后,bAO对象会被回收销毁,下次调用b时再重新创建

image.png

其实编译器在遇到函数的时候,会创建一个 [[scope]] 属性,它即是该函数作用域属性,但是该属性不可访问,所以无法用console.log打印,但是通过xxx.prototype函数查询函数原型,可以看到该属性,例如查询下列函数a的原型

    var a=100
    function b(){
        var b =222
    }
    b()
    b.prototype  //查询函数原型

执行结果如下图

image.png

图中的[[scopes]] 是函数b的作用域内的属性名的集合,因为函数b是在全局变量下的,它的作用域就是全局作用域,所以AO = GO

那么问题来了,如果函数b不在全局,而是在其他函数体内呢? 例如:

function a(){
    var floorA =100
    function c(){
        var floorC =300
        function d(){
            var floorD =400      
            function b(){
                var floorB =200
                console.log(floorB);   //打印200
                console.log(floorD);   //打印400
            } b()
            console.log(floorD);      //打印400
        } d()
    } c()
  console.log(floorB);          //报错
} a()

上述作用域范围 a > c > d > b,函数b在最内层,当想要在函数 a 内,函数c外打印floorB时,却发生了报错,这是为什么呢,其实这就是作用域链原理的问题了,上述代码编译时,执行逻辑如下

image.png

按理说,输出结果就应该是 200 400 400 200 但实际上它却会报错,且b函数还打印了不在其内定义的floorD,这一切都是作用域链在作祟

上面提到,编译器每遇到一个作用域,就会在生成一个对饮的[[scope]]属性,如函数a有aAO,c有cAO,d有dAO,b有bAO,而这几个执行期上下文是包容关系的,即 bAO >dAO > cAO >aAO 也可以理解为 bAO.dAO.cAO.aAO scope属性是作用域内的属性名的集合,图解如下

image.png

3.作用域链

用链式的存储结构将函数的作用域链接起来,就形成了作用域链

函数的作用域既包括它自身,还包括它的上级函数,直到全局

每个函数都有一条scope chain(作用域链),越内嵌的函数,作用域链越长,可知,上级函数的作用域内并不包函其下级函数的作用域,因而,函数a里的 console.log(floorB)无法访问到floorB,故报错,同样的,函数b的作用域链内包含了函数d的作用域,所以它可以访问到floorD,所以函数b里的console.log(floorD)能够打印。

此外,越内嵌的函数,销毁得越早,所以,在函数c还未执行完之前,函数b已经执行完并销毁了。

相信看到这里,你应该已经理解了何为作用域链了,如有疑惑,欢迎在评论区探讨!!!