作用域链

101 阅读8分钟

概述

这篇文章主要介绍的点要下面几个:

  • 什么是GO
  • 什么是AO
  • 什么是作用域
  • 什么是作用域链

GO

全局执行期上下文 Global Object,简称GO,用我们可以理解的词语来指代就是作用域,存放着我们定义的各种变量和函数。

全局:写在最顶层的代码,且不包含在任何函数中的代码就是全局代码,在全局声明的变量就叫做全局变量。下面有一段js代码。

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

当我们的代码被执行的时候,会产生一个执行期上下文,执行全局代码所产生的执行期上下文就叫做GO(Global Object)。这个GO就保存这我们定义的一系列变量还有函数。

在浏览器中我们可以通过全局对象window来访问我们全局所定义的函数还有变量。

image-20230403234049107

AO

全局所产生的执行期上下文叫作GO,我们函数的执行也会产生一个执行期上下文,但不叫做GO而叫作AO(Activation Object)。

var a = 123;
function b() {
	var c = 234;
}
b();
  1. 首先执行全局代码会产生一个GO对象,全局的执行期上下文,里边保存着变量a和函数b
  2. 当执行到b()的时候,b函数执行,这个时候会产生一个新的执行期上下文AO,这个AO里边保存着变量c,当我们去访问c的时候可以看到结果为234
  3. b函数执行完毕,生成的这个AO也会跟随着执行完毕而被销毁

作用域

先看一段代码

function test() {
    var a = 123;
    console.log(a);
}
test(); // 123

{}里面就可以当做是一个作用域,我们可以访问这个作用域里面定义的变量,所以输出123是肯定的。

再来看一段代码

var a = 123;
function test() {
    console.log(a);
}
test();

执行结果是输出“123”这个是肯定的也毋容置疑,但你有没有想过是为什么呢?

都知道我们函数的执行都会产生一个AO,这个AO代表的就是我们的作用域,但明明test函数的作用域中没有变量a,为什么能够访问呢?

你可能会想到,函数自身的作用域里没有变量a,所以会向上层作用域中寻找,直到找到为止。这样想肯定是没错的,但是它内部还有更深层次的逻辑。

还有一个问题就是,在外边不能访问函数里边的变量,这又是为什么?如下:

function a() {
    function b() {
        var c = 123;
    }
    console.log(c);
}
a(); // Error: e is not defined

作用域链

初识[[scope]]

首先,对象呢都可以有属性和方法,我们的函数其实也是一个对象,所以呢它也可以有属性和方法,在学习函数的时候,你可能接触过一个属性,如下:

function a() {
    
}
console.log(a.name);	// a.name

函数上面会有一个name属性,表示这个函数的名字,这也进一步证明了函数也是一个对象。

这些属性没什么特殊的,可以访问可以修改,我们可以拿出来进行使用,但是有一些属性我们没有办法使用,在函数中就有一个属性叫作[[scope]],scope翻译过来叫作区域的意思。

function a() {};

a.name;			// 有这个属性,我们看的见也可以使用
a.[[scope]];	// 有这个属性,我们看不见也不能使用

当运行上面这段代码的时候,a函数并没有执行,而是只经过了函数定义的过程,但就是经过了这个过程a函数上边就有了一些属性,比如我们可以访问的name或者prototype属性。还有一个我们没有办法访问的属性,它是隐式的属性,是a.[[scope]]

[[scope]]:这个属性里边就是存着我们的作用域,也就是执行期上下文(AO/GO)

了解[[scope]]

既然是作用域,那必须得配上一段代码来解释

var g = 123;
function a() {
    var aa = 234;
};

就简单两句话,但是这两句话可不简单,我们一步一步来看:

  1. 生成GO执行全局代码,所以现在GO是这样子的GO {g: 123, a: function a() {}}
  2. 其实就已经执行完了,但是我们关键看a.[[scope]]里边存着什么东西
image-20230404003855252

运行完上边的代码,这个a函数的[[scope]]属性就是这样的

  • [[scope]]的存储结构就像是一个数组
  • 数组的每一项就是执行期上下文,也就是作用域
  • 因为这个函数是定义在全局的,而全局有一个GO,所以现在[[scope]]的第0项就指向GO

那么这是a函数定义的时候[[scope]]会存储的东西,那么函数总会被调用,那么调用的时候又会发生一些变化了,如下我们调用a函数

a();	// a执行

函数执行会产生一个全新的AO,然后加入到[[scope]]的最顶端,就是添加到作用域的最顶端,如下图

image-20230404105156263

上图是a函数执行时[[scope]]的变化

  • a执行会产生一个全新的执行期上下文AO
  • 加入到a.[[scope]]的最顶端

上面的代码已经研究透了,下面在看这一段

var global = 123;
function a() {
    var aa = 234;
    function b() {
        var bb = 345;
    }
    b();
}
a();
  1. 首先到a执行这里的过程[[scope]]里存着什么大家都知道了,就是上面那张图
  2. a执行引发b的定义,那么b的定义也会有一个[[scope]]属性,这个时候它存着的内容是这样的
image-20230404111937105

这个图是不是看着很眼熟,没错就是a执行时[[scope]]所存的内容,因为b函数的定义是发生在a函数的执行里面的,所以b函数是站在a的肩膀上看世界,直接把a的[[scope]]拿过来变成自己的东西。

  1. 接下来到b函数执行,这个时候会产生一个新的AO添加到[[scope]]的最顶端,如下图
image-20230404111833185

现在b的[[scope]]就存着三个作用域,分别是:

  1. b AO

  2. a AO

  3. GO

好了,到了这里应该对函数定义和函数执行[[scope]]的变化有了一定的了解。这个时候我们的[[scope]]就形成了一个链,最顶端是链头,最低端是链尾。

当我们去访问一个变量的时候会从作用域链的最顶端往下依次查找,直到找到为止。这就是为什么自己的作用域没有这个变量,而可以访问到上层作用域变量的原因。

到这里就可以回答上面的一个问题就是“我们为什么不能在外面访问函数里面的变量?”,就以下面的代码为例

function a() {
    function b() {
        var bb = 123;
    }
    b();
    console.log(bb);
}
a();
  1. a defined(a函数定义时的[[scope]]

    1. GO
  2. a doing(a函数执行时的[[scope]]

    1. a-AO
    2. GO
  3. b defined(a函数执行引发b函数定义的[[scope]]

    1. a-AO
    2. GO
  4. b doing(b函数执行时的[[scope]]

    1. b-AO

    2. a-AO

    3. GO

从这里应该可以找到答案了,因为a的作用域链上面并没有b的AO,而变量bb是定义在函数b中的,所以没有办法访问到。

深入[[scope]]

看一段代码

function a() {
    var aa = 123;
    function b() {
        aa = 0;
    }
    b();
    console.log(aa);
}
a();

首先抛出两个个问题

  1. a的执行会导致b的定义,定义的时候b.[[scope]]就已经拿到a-AO了,这个AO是跟a.[[scope]]最顶端AO是同一个吗?
  2. 函数执行完后需要销毁执行期上下文,是怎么销毁的?

先看第一个问题:

b函数执行的时候它的作用域链式这样子的。

image-20230404124744675

去访问a函数赋值为0的时候,会先找第0位的AO里面有没有,发现没有就找到下一个AO。

这个时候在a-AO找到了变量aa并把它重新赋值为0

所以最终结果输出0

再看第二个问题:

当运行完aa = 0这句代码之后是不是这个函数就执行完了,执行完之后作用域链会变成这个样子

image-20230404125155031

作用域最顶端不再是自己运行所产生的AO了,中间联系的那条线就断了

这个时候你可以发现,函数执行完后,[[scope]]就回到了函数刚被定义的状态等待下一次被执行。

这是b执行完后的作用域链。

下面是a执行完后的作用域链

image-20230404132426583

可以看到自己的AO已经找不到了,所以对应的函数b也没了

作用域链只剩下一个GO

a函数回到了刚被定义的状态,等待下次被调用执行。