作用域链的深入浅出

626 阅读7分钟

在了解作用域之前,首先要介绍一下预编译。 预编译是做些代码文本的替换工作。是整个编译过程的最先做的工作。这个工作是发生在代码执行之前,由浏览器引擎去完成。

此时,会出现两种情况: 一种是发生在函数执行之前。 一种是发生在全局。

本文用个经典的例子来描述预编译的过程和作用域链的介绍:

function fun(a,b){
                console.log(a);   //ƒ a() {}
                console.log(b);   //undef
                var b = 234;
                 console.log(a);   //ƒ a() {}
                console.log(b);   //234
                a = 123;
                console.log(a);   //123
                console.log(b);   //234
                function a(){   
                 }
                console.log(a);   //123
                console.log(b);   //234
                 var b = function(){}
                console.log(a);   //123
                console.log(b);   //ƒ () {}
            }
            fun(1)

发生在函数执行之前 四部曲

1.创建AO对象(Activation Object)

在函数fun()执行之前首先要创建fun的AO对象.这里我用funAO来表示.
funAO{

}
此时funAO对象里面是空的,接下来就要进行第二步操作。

2.找形参和变量声明,将变量声明和形参作为AO的属性名,值为undefined

注意!!被声明的才是变量,很容易将赋值操作和变量声明弄混淆。 函数fun()中声明的变量有 var b;形参有 a,b。 此时该如何存入进funAO{}中呢? 将变量声明和形参作为AO的属性名,值为undefined。因为编译过程从上至下,所以首先将a、b存入funAO{}中,a、b作为属性名,值为undefined. funAO{ a: undefined, b: undefined } 接下来还有变量var b,但此时funAO中已经有了b,并且也没有别的变量,那么第二步到此就结束了。接下来进行第三步操作.

3.将实参和形参统一

由于fun()函数在调用时传了一个实参1,对应fun(a,b)中的a;而b并没有传参数进来.所以此时将实参和形参统一的操作就为
funAO{
    a: 1,
    b: undefined
}
此时第三步已经完成,接下来开始第四步.

4.将函数体内找函数声明,将函数名作为AO对象的属性名,值赋予函数体

fun()中的函数声明有function a().
funAO{
    a: function a(){},
    b: undefined
}
此时函数预编译已经完成,接下来要开始执行fun()函数了,函数体内的赋值操作时根据funAO{}中的属性名来修改该属性名对应的值。
funAO{
    a: function a(){},
    b: undefined
}
         function fun(a,b){
             console.log(a);   //ƒ a() {} 读取funAO中的a 值为该函数体
             console.log(b);   //undef 读取funAO中的b 值为undefined
             var b = 234;      //此行代码对b进行了一个赋值操作 b =234 此时funAO中的b:undefined被修改成b:234.
             console.log(a);   //ƒ a() {}
             console.log(b);   //234 由于上面将funAO中的b:undefined 修改为b:234所以读取到的b为234
             a = 123;//此行代码对a进行了一个赋值操作 a =123 此时funAO中的a: function a(){}被修改成a:123.
             console.log(a);   //123
             console.log(b);   //234
             function a(){
                 
             }
             console.log(a);   //123
             console.log(b);   //234
             var b = function(){} //此行代码对b进行了一个赋值操作 b = function(){} 此时funAO中的b:234被修改成b = function(){}.
             console.log(a);   //123
             console.log(b);   //ƒ () {}
         }

搞明白以上每一步,相信你对函数的预编译有进一步的了解,接下来介绍发生在全局的预编译。

发生在全局 三部曲

其实在全局的编译与在函数体内的编译大体相同,接下来我们一起来看看发生在全局的编译

将上述例子稍作修改,简化了函数体内的内容,并在函数体外部声明了一个变量将变量作为参数传进fun()函数中

var i =1;
function fun(a,b){
               console.log(a)
            }
            fun(i)

1.创建 GO 对象

GO{
}
当GO对象被创建完时,立马执行第二部

2.找全局变量声明 , 将变量声明做为GO的属性名 ,值为undefined

全局变量有var i,此时把i放入GO{}对象中 i作为属性名,值为undefined.
GO{
    i:undefined
}
此时已经没有别的全局变量,第二步执行完毕立马执行第三步

3.在全局找函数声明,将函数名作为GO对象的属性名,值赋予函数体

此时函数声明为function fun,将函数名fun放入GO对象中作为属性名,值为函数体
GO{
    i:undefined,
    fun:function fun()
}

此时你会发现发生在全局和发生在函数体上的操作基本雷同,但是上述例子我们时分开来进行阐述,往往在实际的过程中,对全局和函数的编译往往时发生在一起的.例如在编译一份js文件时,先要从全局开始编译直到调用到某函数的前一刻,开始编译该函数体.当我们需要对一个变量进行操作时,往往从所创建的AO或GO对象中找到相应属性名来对值进行操作,如果对应AO对象中没有我们所需要的属性则会由子级作用域返回父级作用域中寻找变量,就叫做作用域链。 作用域链中的下一个变量对象来自包含环境,也叫外部环境。 而再下一个变量对象则来自下一个包含环境,一直延续到全局执行环境。 全局执行环境的变量对象始终都是作用域链中的最后一个对象。

那么什么是作用与链呢?

作用域链

在介绍作用域链之前,我们需要先了解一些事情函数本身是对象,是对象就有属性 当一个函数被创建时,函数自带:

函数名.name 属性 //函数名

函数名.prototype //原型

函数名.[[scope]] //作用域属性 隐式属性 不可访问属性

当一个函数在执行的前一刻 ,会创建一个AO对象 ,在函数执行完成之后AO对象会被回收

-执行期上下文(AO对象)

当函数在执行的时候,会创建一个称为执行期上下文的内部对象(AO对象)。一个执行期上下文(AO对象)定义了一个函数执行是的环境,函数每次执行时对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行期上下文,当函数执行完毕他所产生的执行期上下文会被销毁。

-查找变量 从作用域链的顶端依次往下查找

-[[scope]]

函数的作用域,是不可访问的,其中存储了运行期上下文的集合

-作用域链

[[scope]]中所存储的执行期上下文对象的集合,这个集合呈链式连接,我们把这种链式连接叫做作用域链

下面我们在举一个例子

function a(){
    function b(){
        var b = 234;
    }
    var a = 123;
    b();
}
var glob =100
a()

现在我们已经了解了代码在预编译时进行的操作,现在我们综合起来对这个过程进行一个阐述.

在a函数被定义的时候,会发生以下过程

image.png

在a函数被执行时,发生以下过程

image.png 此时在a函数执行的过程中,定义了函数b.

b函数被定义时,发生以下过程

image.png 在这之后,就开始执行b函数,当b执行完成之后,便会销毁b的AO对象,依次类推,当a执行完成后便会销毁a的AO对象.而且如果在执行b函数时,需要对某个变量进行操作时,会优先从b的AO对象中寻找该属性,如果没有便会根据作用域链往上一级找,直到找到第一个匹配的属性名停止,进行修改.这就是为什么外层函数访问不了内层函数的变量而内层函数可以访问外层函数的内容.

总结

希望本篇文章能对初入前端的小伙伴有所帮助,能够更加清楚的认识作用域链的原理.也是对我个人学习的归纳总结.首次在掘金平台上发布文章,希望路过的大佬们进行指点和提供建议.