37DATA

生动形象带你重温执行上下文、作用域与作用域链

一、前言

想必大家在开发过程中,特别是前端的同学,对于执行上下文、作用域以及作用域链这几个术语应该都不陌生,甚至我们开发过程中就已经涉及其原理,但现在如果突然让我们来对其解释一波,往往我们就会语无伦次了,那么这节课我会结合图例以比较生动地形式带大家来重温一下执行上下文、作用域以及作用域链的概念及原理。

二、执行上下文

执行上下文可以说是评估和执行JavaScript的环境的抽象说法,简单说就是JS执行代码将在哪个环境中执行;它要在JS代码执行前进行创建,具体的创建流程咱们后面再说。

首先,执行上下文可以分为

  • 全局执行上下文

  • 函数执行上下文

  • eval函数执行上下文

我们的JS代码解析执行时,全局执行上下文可以说是最基础最根基的环境,一开始就会创建,而且!!!全局执行上下文一个程序有且只有一个,接着,在执行到函数的时候,又会生成对应函数的执行上下文,也就是对应的执行环境,函数执行上下文可以有多个,eval函数执行上下文亦是如此,这里eval比较少用就略过了。讲到这里你可能还是懵懵的,你可以先类比上下文环境中存在你代码执行时所需要的一些东西。

Image

接着,从执行上下文又牵出了执行栈,按代码解析执行所产生的执行上下文顺序依次压入栈,按照先进后出的顺序,每次执行完栈顶的代码后就将其上下文pop弹出,等待垃圾回收,进入下一个执行上下文,执行完则重复如上操作。那么下面讲一下执行上下文的俩个阶段吧,创建阶段(第3-5点)和执行阶段(第6点)。

三、this 指向的绑定

全局执行上下文中,this指向全局对象,Browser中是window对象,Node中是global对象;函数执行上下文中,this取决于被谁调用,函数被引用对象调用时,this指向该对象,否则指向全局对象或者undefined(严格模式)

Image

四、创建词法环境组件

什么是词法环境呢?简单来说就是执行环境中(执行上下文中)存储变量和函数声明的实际位置,以及定义该执行上下文中能访问到的变量等(即创建作用域链),换种说法我们可以将其当作一个“区域”,包裹在执行上下文中,那么词法环境由以下俩部分组成,分别为

  • 1、环境记录器(变量对象 - Variable Object):存储变量、函数声明的实际位置,与作用域息息相关,保存当前一级作用域下的所有函数和变量,注意Variable Object不存储函数表达式;

  • 2、外部环境的引用(是个类 Variable Object):可以访问父级词法环境(作用域),其内容为当前词法环境的变量对象 + 父级词法环境变量对象,这也是创建作用域链的关键。

词法环境类型:

  • 1、全局环境(其变量对象即为全局对象 - Global Object,唯一浏览器可访问的变量对象,被具象给了window),其外部环境引用为null,最根的词法环境了,其环境记录器有window对象、用户自定义的全局变量、原型函数等等,在这个环境中this指向全局对象;

  • 2、函数环境,函数内部定义的变量,函数声明等记录在环境记录器中,其外部环境引用可能是全局环境,也可能是函数环境。

环境记录器类型:

  • 1、对象环境记录器(全局环境)

  • 2、声明式环境记录器(函数环境)
    PS:函数的arguments对象在该环境记录器里

搬了一段伪代码过来,方便大家对上面内容进行理解

Image

在创建函数词法环境的环境记录器,即变量对象时,会先创建一个活动对象 - (Activation Object),Activation Object被初始化为[arguments],随后被并入到后续初始化的变量对象中去,这也就是环境记录器中有arguments(又或通俗点说我们在函数中能够使用arguments)这个变量的原因。

五、创建变量环境组件

变量环境也是一个词法环境,这是ES6之后才有的一个环境,那ES6后为什么要从词法环境里再单独拆出来一个变量环境呢?因为ES6后拆出来的这个变量环境的作用是用来存储var的变量绑定的,而前者现在则是用来存储函数声明和变量(let 和 const)的绑定,相当于把 var 的变量绑定独立出来了。看下下面这个例子

Image

Image

图中可以看出在词法环境中,let 和 const没有关联任何值,而在变量环境中var定义的变量初始值被设为了undefined,这也就是es6中用var定义的变量未声明赋值前你可以调用,而let 和 const定义的变量却会得到一个引用错误的原因,换种更熟悉的说法就是var的变量声明提升,它被存储进了变量环境赋予了初始值undefined。额外补充,函数声明提升还是在变量声明提升前面,这点没有变化记得。

六、执行阶段

完成存储好的变量的分配,就可以执行代码了,还有,函数执行上下文是在函数调用前才开始创建的,不是一开始就全创建好的。

七、作用域与作用域链

好了,说到这里,是不是突然会有种感觉觉得执行上下文中的词法环境跟作用域很像,但它们是不一样的,不多说,直接上例子。

var A = 1;
function test(){
let B = 2;
function inner(){
const C = 3
console.log(A)
}
}

Image

上面声明定义A,B,C变量时作用域就已经定义好,因为JS的作用域是静态作用域,初始化时即定义,所以这里就定义好了执行时各变量的绑定关系,取哪里的值。

那在执行时比如上面的C函数里输出A要怎么去找到父级作用域的A,这时就需要通过词法环境里的外部环境引用去取得父级词法环境里的变量A。

首先,我们这里把A当全局执行上下文,在执行全局执行上下文时,其会将当前作用域下的变量、函数声明等进行初始化,形成变量对象,然后复制A的一个私有属性 [[Scope]] (这个属性就是用来管作用域的,函数一定义时就生成了,上下文执行中可以访问到该属性,保留着自身作用域绑定关系及相关指针),然后将变量对象拷贝并到一起形成 [[Scope Chain A]] 对象,也就是作用域链的链头。

接着,B的词法环境的外部环境引用保存着一个指向A的 [[Scope Chain A]] 的指针,这里B的执行上下文就可以通过自己的外部环境引用里 [[Scope Chain A]] 的指针摸上去访问到A的变量信息了,同时将 [[Scope Chain A]] 的指针和自己的变量对象、[[Scope B]]合并成 [[Scope Chain B]] ,同理,C也是如此,其通过外部环境引用里的 [[Scope Chain B]] 的指针, [[Scope Chain B]] 里又有 [[Scope Chain A]] 的指针,那么就可以依次访问到B和A的变量信息了,环环相扣,是不是一条链,C的作用域链就构建完成了。

总的来说,作用域是初始化时定义(变量的取值绑定),作用域链是创建执行上下文时形成(怎么去拿),执行上下文创建阶段通过外部环境引用就已经联系起来了。

但但但是,还有一个特别例子,当某个函数返回匿名函数时,也就是我们常说的闭包,管作用域的私有属性 [[Scope]] 决定了在执行上下文被销毁时,由于某个变量还需要被用到,被绑定引用着,该变量对象在内存中会被单独留了下来供后续使用,因而没被垃圾回收机制回收。

八、最后

好了,那么以上就是这节课的所有内容了,执行上下文、作用域与作用域链这仨个知识点相对其他来说还是比较抽象的,特别是执行上下文,不能比较好的具象化出来,所以课后还需要大家去慢慢地对它们消化一波,相信在进一步的理解温固之后,能够让大家有所收获,对这仨个知识点也能够进一步的认识。如果上文有什么不对的地方,也欢迎大牛指出纠正,谢谢!

借鉴参考链接:

  • https://juejin.cn/post/6844903682283143181

  • https://github.com/kuitos/kuitos.github.io/issues/18