Go to comments

JavaScript 作用域链

一个函数就像一个屋子一样,这个屋子好像形成了一个单独的"域"和外界有一些阻隔,屋子里面能看到外面,外面看不到到屋子里面,而且屋子与屋子之间是彼此独立的。


可以把函数所生成的空间,管它叫做一种"作用域",但是不精准,下面就真正的探索一下作用域是什么?


作用域确实是因为函数的产生而产生的独特东西,作用域属于一个函数,一个函数产生了一样的作用域,这两个是相绑定的,作用域到底是什么呢?首先了解一下[[scope]]

一、作用域 [[scope]]

首先我们知道,每一个对象都有两个基本特性,有属性、有方法。对象可以有属性,一切为对象的东西就都可以有属性(古希腊哲学的三段论)。


函数也是一种特殊对象,因为函数也有属性比如name属性,函数是特殊类对象叫函数类对象

函数类对象有一些我们可以用的属性,比如 test.name 包括后期学的 test.prototype ,这些都是我们可以访问的属性。

function test(){

}

console.log(test.name); // name属性返回函数名test

还有一些属性是我们访问不了的,比如 test.[[scope]] ,scope翻译过来有区域的意思,计算机术语叫"域"的意思。


[[scope]]是什么的呢?

test.[[scope]]里面存的就是由这个test函数产生而产生的的作用域,[[scope]]里面存的就是一个作用域,里面很复杂的结构下面会详细学,现在就知道[[scope]]里面存的是一个作用域就OK了。


为什么[[scope]]属性我们能不能用呢?

因为这是隐式的属性,隐式的属性就是说,他是属性但是我们用不了。


这个属性是干什么的呢?

系统会通过它内部的一些原理,会定期的调用[[scope]]但不会让我们用的。下面是[[scope]]属性的官方解释。

[[scope]]属性的官方解释:


[[scope]] 指的就是我们所说的作用域,作用域中存储了运行期上下文的集合。

执行期上下文预编译学了一些,这两节课特别有意思,

学闭包时必须要知道执行期上下文,但执行期上下文的精解又属于预编译的环节(闭包和执行期上下文有不挂钩),上次学的AO、GO就是行期上下文。下面看执行期上下文的官方解释:

执行期上下文官方解释:


当函数执行时,会创建一个称为 执行期上下文 的内部对象。

注解:这句话精准的理解,是当函数执行的前一刻,因为预编译发生在执行的前一刻。执行期上下文简称AO

一个执行期上下文定义了一个函数执行时的环境

注解:泛泛的理解一下,什么是函数执行的环境,由于这个函数而产生的一些变量提升、函数提升


一个执行期上下文定义了一个函数执行时的环境,函数每次执行时对应的执行上下文都是独一无二的,所以多次调用一个函数会导致创建多个执行上下文,当函数执行完毕,执行上下文被销毁。

现在执行test()函数产生一个AO,执行完了再执行下一次,下一次新建了另一个AO和上一个的AO就没关系了,因为这是两次执行。两次执行就有两次环境,所以每次执行完之后,都会产一个生独一无二的AO执行期上下文

function test(){
}

// test() --> 第一次执行产生的AO{}; 
// test() --> 第二次执行产生的AO{};

第一次执行完后AO上哪去了?

AO只作为即时的存储空间用完就销毁了。就是一个函数执行的时候产生的AO,执行完AO就销毁掉。


看完执行期上下文的概念,回来再看作用域[[scope]]属性:

[[scope]] 指的就是我们所说的作用域,其中存储了运行期上下文的集合。

一个函数里面有一个[[scope]]属性里面存的是作用域,一个函数执行的时候还产生一个唯一的AO,为什么里面放的是一个集合呢?别着急下面继续学。


成哥在这块解释一下:[[scope]]里面存的其实叫"作用域链",这个作用域链才是执行期上下文的集合。


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

也就是说[[scope]]里面存的是一个作用域链,这个作用域链是由多个执行期上下文链接起来的,像一个链条一样链接的结构。


是那来的,除了函数自己产生的执行期上下文,那些不是自己产生的执行期上下文是哪来的?

二、作用域链精解

举例说明 [[scope]] 

function a(){

    function b(){
        var b = 123;
    }

    var a = 123;

    b();
}

var glob = 100;

a();


/****

第一步:a函数被定义
a defined a.[[scope]] --> 0 : GO{}

第二步:a函数执行
a  doing a.[[scope]]  --> 0 : AO{} 
                          1 : GO{}

****/

第一步:a函数定义:

在函数a,刚刚出生的时候(函数的定义环节)就有属性和方法了,比如 a.name 属性还有 a.[[scope]] 属性。

a函数刚刚被定义的时候,a.[[scope]]里面就存了东西了,存的是全局的 GO。


a.[[scope]] 里面存的是作用域链(看下面图),作用域链里面装的是执行期上下文集合,这个时候还没有达到集合的程度,作用域链里面只存了一个第0位,第0位指向的 Global Object 就是GO,GO是全局的执行期上下文。

pic1586943695470406.png

GO里面有一堆东西,有a函数、global变量等,都是GO里面应该有的东西。现在作用域链只有一个GO,还没有形成链结构。

a函数被定义完,接着走的环节是a函数执行。


第二步:a()函数执行

a()函数执行的时候发生了一个改变,一个函数执行的时候要产生一个执行期上下文(AO),a函数产生的AO放在哪里呢?


先想一个问题,为什么在一个函数里面去访问变量值的时候,是先找函数自己的AO,然后再往上面去找?

比如下面test函数里面访问变量a,函数执行a()打印结果是123

var a = 234;

function test(){

    var a = 123;

    console.log(a);

}

test();// 打印的是123

test函数自己的AO里面有变量a,全局GO里面也有变量a,函数先找自己AO里面的。也就是说先找函数自己AO再找GO的,肯定是函数自己的AO和GO有一点什么联系,这种联系就叫作用域链。


回到a函数:

当函数a()被执行的时候,产生自己的AO(执行期上下文)放到哪里了呢?放到作用域链的顶端,然后把GO往下放一位,形成了a函数自己的作用域链

函数a()执行的时候里面有个 a.[[scope]] 属性,里面存的是作用域链叫 scope chain 有两位,第0位指向的是a函数自己新产生的AO,第1位指向的是GO。

pic1586943695470406.png

所以这时候要在函数a里访问一个变量,系统到 a.[[scope]] 里去找这个变量,a.[[seope]]里面放的是一个链(有顶端、有底端),系统会到作用域链的顶端去找,然后依次下向的去找。

上一节课说要找一个变量到AO里找或GO里去找,其实标准说法是到函数的作用域链里面找,然后顺着作用域链从顶端找到作用域链的底端,这是整个查找的过程。


再整理一下

查找变量:从作用域链的顶端依次向下查找。

上面这句再补充一下,在哪个函数里面查找变量,就到哪个函数的作用域链顶端依次向下查找。

比如在a函数里面找一个变量,我们应该找a.[[scope]],因为a.[[scope]]已经是一个作用域链了,a函数怎么构成的作用域链?

a函数刚出生(定义)的时候,a.[[scope]]就已经存了GO,再形象点说a函数刚刚出生的时候,a.[[scope]]存的是他所在环境的执行期上下文,a函数所在环境是全局的所以他存的是GO。


第三步:b函数的定义

由于a函数的执行,产生一个过程叫 b函数的定义,b函数作为一个函数,他也有自己的b.[[scope]]

当b函数刚出生的时候,他的环境不但在GO里面还在AO里面,b函数的环境是a()函数执行形成的环境。

所以b函数定义时候的b.[[scope]拿的现成的,就是a()函数执行时候一模一样的劳动成果a.[[scope]

pic1586943695470406.jpg

然后这个阶段没什么用,b函数定义的时候也访问不了,关键在b函数执行时候发生的事。


第四步:b函数执行

b函数在执行的时候发了什么事?b函数也得生成了一个自己的AO(执行期上下文),放到了b函数自己作用域链的最顶端。

如果在b函数里面访问一个变量,访问顺序是自顶部向下的去访问,自b.[[scope]]第0位向第2位去找。

pic1586943695470406.jpg

重新整理一遍:

第一步:

a函数定义在全局的作用域里面,a函数定义有一个a.[[scope]],a.[[scope]]里面存的是全局的执行期上下文。然后紧接着a函数被执行了。


第二步:

a函数被执行产生里一个AO,a函数把自己的AO放到作用域链的最顶端,形成一个新的作用域链

也就是说现在a.[[scope]] 和 刚才定义时的a.[[scope]]存的值不一样了,a.[[scope]]还是原来那个。

这个时候如果访问a函数里面的变量,访问的是a.[[scope]],遵循自顶向下的规则去查找。


第三步:

接着,a()函数的执行产生了b函数的定义,因为b函数在a函数体内,a函数执行了b函数才能被定义。

b函数被定义的时候,b函数有自己的[[scope]],b.[scope]]里面存里b函数的作用域链。


但是b函数的作用域链和a函数的作用域链不一样,a函数在全局的环境下出生,能看到的范围就是全局的。但是b函数出生的时候,可是站在a函数的肩膀上看世界的。所以a函数里面能看的东西,b函数都能看到。

所以b函数刚刚创建(定义)的时候,b.[[scope]]拿到的是a函数的劳动成果。

然后b函数被执行


第四步:

然后b()函数被执行,b被执行的时候,b函数自己产生了一个执行期上下文。

b函数产生的执行期上下文后,放到自己作用域链的最顶端形成一个作用域链。

然后在b函数里访问变量,会依照b函数的执行期上下文的顺序 0 -> 1 -> 2 访问。

三、作用域链精解中的问题

1、b函数作用域链的第1位的AO,是不是a函数的AO?

先看一个小例子:

1). a函数里面有变量aa,在b函数里面能访问变量aa

2). 在b函数里把变量aa的值变成0了

3). 然后b()函数的执行把变量aa变成0了

然后在b函数执行完之后访问变量aa,变量aa的值输出什么?

function a(){

    function b(){

        var bb = 234;

        aa = 0; // 1. 在b函数里面,是可以访问到变量aa,把变量aa的值变成0

    }

    var aa = 123;
    
    b();
        
    console.log(aa); // 2. b函数执行完,访问变量aa

}

var glob = 100;

a(); // 0

现在讨论的是,b函数执行期上下文中第1位的AO,和a函数执行产生的AO,是不是同一人?

a()函数执行输出0,b函数作用域链里面的第1位AO,就是函数a产生的那个AO,就是同一个人。再换者说全局的GO总是一个人吧,但凡一个作用域链,必须存的都是同一个GO。

每一个函数自己都有一个执行期上下文的集合叫作用域链,我们真正在一个函数里面去访问一个变量,要遵循这个函数所产生的作用域链去访问。

2、研究一下函数执行完的一个顺序

a函数定义a()函数执行,b函数定义b()函数执行,b函数执行完之后呢?

一个函数执行完要销毁他的执行期上下文。


看图:

b函数执行完之后,把他自己执行期上下文的线去掉了(不指向那个空间了),相当于销毁了自己的执行期上下文。

销毁了之后,b函数又回归到它被定义的状态,等待下一次被执行。

pic1586943695470406.jpg

b函数执行是最后一句,b函数执行完后相当于a函数也执行完了。


a函数的作用域链就两位,第0位存的是a函数自己的AO,第1位存的是GO。

a函数执行完a.AO也销毁了,a函数回到定义状态,等待下一次被执行。

但是a函数里面有一个b函数,a函数把线剪断后,b函数不用等待下一次被执行了。

pic1587818134262799.png

a的执行期上下文里存的是b函数,a函数指向的a.AO的线剪断了,b函数不用等待下一次执行,因为b函数不复存在了。

不用管b函数了,刚才的b函数永远没了,a函数又回回归到被定义状态,等待下一次被执行。


当a函数下一次被执行的时候,又产生一个独一无二新执行期上下文,这个新执行期上下文,再放到a函数自己作用域链的最顶端。刚才产生的执行期上下文已经没有了。

然后由于产生了一个新的执行期上下文,又产生了一次b函数的定义,又一个全新的b函数呈现出来了,这个全新的b函数又保存了a函数的劳动成果,再一次等待被执行。

b函数被执行的时候,产生了一个全新的执行期上下文,到自己作用域链的最顶端,然后形成一个作用域链,等待被查找变量。

四、写一个小例子练一下

function a(){

    function b(){

        function c(){
        }

        c();
    }

    b();

}

a();


/***

a defined a.[[scope]] --> 0 : GO     a函数定义

a doing   a.[[scope]] --> 0 : a.AO   a()函数执行
                          1 : GO

b defined b.[[scope]] --> 0 : a.AO   a函数执行完产生了一个b函数定义,b.[[scope]]拿的是a.[[scope]]的劳动成果
                          1 : GO
                          
b doing   b.[[scope]] --> 0 : b.AO   紧接着b()执行,第0位是b产生执行期上下文,放到作b函数做用域链的最顶端
                          1 : a.AO
                          2 : GO

C defined c.[[scope]] --> 0 : b.AO   函数b()的执行产生了函数c的定义,c.[[scope]]存的是b函数的劳动成果
                          1 : a.AO
                          2 : GO

c doing   c[[scope]] -->  0 : c.AO   函数c()执行,产生自己的AO
                          1 : b.AO
                          2 : a.AO
                          3 : GO 

***/

1、在任何一个函数里,想访问一个变量就找他的作用域链

找变量肯定是函数执行时候产生的作用域链,执行时候生成的作用域链才是真正的作用域链。

记住一个事,这里面所有的a.AO都是一个人,b.AO也是一个人,只不过互相借用了一下而已。

2、c函数执行完会做什么?

c函数执行完会扔掉自己的执行期上下文,把c.AO干掉,c函数回归到被定义状态,如果在这个状态上,在执行一次c()函数呢?


会生成一个同样类似于c.[[scope]]的环节,类似就是除了b.AO、a.AO和GO一样,c.AO就不一样了,这次的是一个new_c.AO。剩下的都一样,因为基于的基础都是c defined(c函数定义)


a defined a.[[scope]] --> 0 : GO


a doing   a.[[scope]] --> 0 : a.AO

                                        1 : GO


b defined b.[[scope]] --> 0 : a.AO

                                         1 : GO

                          

b doing   b.[[scope]] --> 0 : b.AO

                                        1 : a.AO

                                        2 : GO


C defined c.[[scope]] --> 0 : b.AO

                                         1 : a.AO

                                         2 : GO


c doing   c[[scope]] -->  0 : c.AO

                                       1 : b.AO

                                       2 : a.AO

                                       3 : GO


c doing   c[[scope]] -->  0 : new_c.AO

                                       1 : b.AO

                                       2 : a.AO

                                       3 : GO 

3、问题维密天使:函数什么时候执行完,什么时候销毁?

执行:

1. c()执行这条语句要等,c函数体里面的语句执行完了,c()执行才算执行完了。

    c()执行语句是b函数的最后一条语句,b的最后一条语句执行完了,说明b函数被执行完了。

2. b函数被执行完了,说明b()执行这条语句执行完,也就是a函数的最后一条语句执行完了

3. a函数的最后一条语句被执行完了,就宣告着a函数被执行完了


销毁:

每一个函数执行完,意味着这个函数产生的执行期上下文被销毁

c()这句非常有意思,叫c函数的执行(调用)语句,调用语句分两个环节,调用的时候是这个函数的执行,调用语句执行完了,这个函数就执行完了。

1. 所以调用的时候,首先c形成自己的执行期上下文,放到自己作用域链的最顶端。

2. 然后当c函数执行完,c函数先自己销毁自己的执行期上下文,说明c函数执行完了,也就宣告b函数执行完了

3. b函数执行完,b函数消耗自己的执行期上下文,然后宣告a函数执行完了

4. 然后a函数执行完,a函数消耗自己的执行期上下文

c函数先销毁,哪个函数先执行完,哪个函数先销毁,销毁和执行完是一个概念。



Leave a comment 0 Comments.

Leave a Reply

换一张