从执行上下文,执行栈到作用域,作用域链,词法环境,变量对象,活动对象,[[scope]],AO, VO

486 阅读10分钟

标题中其实有重复的东西,比如作用域其实就是词法环境,所以让我们整理一下。

  • 执行上下文(Executing Context)
  • 执行栈(Executing Stack)
  • 作用域(Lexical Environment)=== 词法环境
  • 作用域链
  • [[scope]]
  • 变量对象 (Variable Object) === VO
  • 活动对象 (Active Object) === AO

引子(从执行上下文与执行栈说起):

JavaScript 中,当JS代码运行时会创建一个栈(给这个栈起个名字叫ES),当代码执行遇到函数调用时,此时会创建一个该函数的 调用信息的结构(给这个结构起个名字叫EC),并将EC压入ES中,当该函数执行完毕时,该函数的EC就会从ES弹出去,等到下一个函数调用继续执行上面的操作。 (暂时不要管EC这个信息结构里到底有什么信息)

如果遇到函数连续嵌套调用的时候,比如调用函数fn1,这时候会创建了fn1的EC,并且将fn1的EC压入ES,执行函数fn1里面的代码时,发现在fn1中调用了fn2,此时继续创建fn2的EC,并将fn2的EC压入ES中。当fn2函数执行完毕时,fn2的EC将会从ES中弹出,此时继续执行fn1,当fn1执行完毕,fn1的EC将会从ES中弹出。

上面引子中的EC就是执行上下文,ES就是执行栈

  • 执行栈:执行栈就是一个栈结构,存放所有的执行上下文,JS运行过程中有且只有这一个执行栈。

  • 执行上下文: 执行上下文其实分为两种,一种是全局执行上下文(Global Executing Context),一种是局部执行上下文(Local Executing Context)。

    • 局部执行上下文:上面调用函数产生的执行上下文就是局部执行上下文,局部上下文可以有多个。

    • 全局上下文:而在整体JS代码开始运行时会创建一个全局上下文,这个全局上下文有且只有一个。

结合代码理解执行上下文与执行栈:

function fn1() {
 console.log('fn1');
 function fn2(){
   console.log('fn2');
 }
 fn2()
}
fn1()
  • 执行这段代码之前先创建执行栈ES,这里用数组表示 ES : [ ],其实在整体代码刚开始执行的时候,上面介绍说了会创建一个全局上下文(这里叫它Global_EC),执行栈ES就会压入Global_EC : ES.push(Global_EC) ,此时的 ES:[Global_EC]
  • 代码执行到fn1()的时候创建fn1的局部执行上下文(这里叫它fn1_EC),ES就会压入fn1_EC: ES.push(fn1_EC),此时的ES:[Global_EC,fn1_EC]
  • 代码执行到fn2()的时候创建fn2的局部执行上下文(这里叫它fn2_EC),ES就会压入fn2_EC: ES.push(fn2_EC),此时的ES:[Global_EC,fn1_EC,fn2_EC]
  • 当fn2函数执行完毕,执行栈ES需要将fn2的执行上下文fn2_EC弹出:ES.pop(),此时的ES:[Global_EC,fn1_EC]
  • 当fn1函数执行完毕,执行栈ES需要将fn1的执行上下文fn1_EC弹出:ES.pop(),此时的ES:[Global_EC]

至此,执行上下文与执行栈大体工作原理应该掌握,下面其实应该说一说执行上下文中保存的信息是哪些,但是在此之前我们需要了解什么是作用域。

作用域是什么

作用域:Lexical Environment,翻译过来就是词法环境(作用域===词法环境):词法环境规定了如何查找变量,也就是确定了代码的访问权限,它是个抽象的东西,不像执行上下文以及执行栈是具体的,你可以把作用域理解为一种规则,一种范围,在作用域这个范围内声明的变量以及函数你都能访问到 。 JavaScript中的作用域是静态的,静态的意思就是在创建函数的时候,其作用域就确定了(如果是在函数调用时候确定的词法环境,是动态词法环境)

举个例子,上代码

var a = 1

function fn1() {
  var a = 2
  fn2()
}

function fn2() {
  console.log(a);  // 输出1
}

fn1()

console.log(a)最终输出的 1 来自全局变量的 var a = 1,为什么,因为fn2声明在全局环境中,不是fn1中,所以fn2的父级是全局环境,当使用a时,由于fn2中没有定义a,所以fn2会顺着作用域链向上找,作用域链的上一层就是父级全局环境,发现了a=1,于是输出了1。

这也佐证了JS作用域是静态的,如果是动态的,那么就是在函数调用时确定的作用域,fn2调用时是在fn1中,那么输出的将会是fn1中声明的var a = 2

至此,我们大概知道作用域大概是个什么玩意,但作用域变量查找规则具体是什么,我们上面似乎只说了顺着作用域链查找,所以具体查找规则我们将放在下面作用域链中具体说,我们说的这些就是为理解下面作用域链做铺垫。

大概了解作用域之后,接着说刚才执行上下文的保存了那些信息的事。

执行上下文创建保存了哪些信息。

主要保存了或者说做了下面三件事

  • 创建了变量对象(VO:Variable Object)
  • 确定了this
  • 创建作用域链

注意:执行上下文创建时,代码还未执行,当执行上下文创建完毕,代码开始执行,其实这就是变量函数提升的原因

1,创建了变量对象(VO:Variable Object)

变量对象就是当前执行环境(当前函数)中定义且能够访问的变量,这些变量以变量对象的属性形式存在,比如你在当前函数中声明 var a = 1 ,这个a就会放到变量对象中保存起来,当你使用到a的时候,就会从变量对象里取出来使用。

变量对象分为全局变量对象与局部变量对象

  • **全局变量对象:在进入全局执行上下文时,创建的就是全局变量对象,包括 **

    • 初始化变量声明

    • 初始化函数声明

举个例子,上代码。

var a = 1
function getName() { }

上面代码开始执行的时候,创建的是全局上下文,此时全局上下文内容(注意观察variable_object的内容就是变量对象的内容,this以及scope_chain暂且不用管):

global_executing_context(全局上下文): {
  variable_object(变量对象): {
    a: undefined,  // 初始化变量声明
    getName : function get() { },  // 初始化函数声明
    // ... 其他JS内置全局变量
  }
  this: window,
  scope_chain(作用域链): null
}

我们可以看到全局变量对象里的内容就是我们当前声明的函数(getName)与变量(a)+原先内置的全局变量,注意这里声明的变量值都是undefined,关于变量的赋值操作将在执行上下文创建完毕代码执行时候赋值(这里也就可以解释变量提升与函数提升是怎么回事,有兴趣的小伙伴可以自行查阅,本文不作深入),在创建执行上下文阶段只会给变量赋值undefined。

  • 局部变量对象:在进入局部执行上下文时,创建的就是局部变量对象,包括
    • arguments对象创建
    • 当前函数的形参初始化
    • 初始化变量声明
    • 初始化函数声明

举个例子 上代码

function killBug(name) {
  var count = 10
  function kill() {
    console.log('kill bugs');
  }
}

killBug('大虫子')

上面执行到killBug('大虫子')的时候,创建的是局部执行上下文,此时局部上下文内容(注意观察variable_object的内容就是变量对象的内容,this以及scope_chain暂且不用管):

local_executing_context(局部上下文): {
  variable_object(变量对象): {
    arguments: { 0:'大虫子', length:1 },  // arguments对象创建
    name:'大虫子',  // 函数的形参初始化
    count:undefined, // 初始化变量声明
    kill:function kill() { console.log('kill bugs'); }  // 初始化函数声明
  }
  this: window,
  scope_chain(作用域链): [GO]
}

我们知道变量对象就是VO,那么AO是什么,AO(Active Object)其实就是活跃的变量对象,比如我们上面调用killBug函数的时候,会创建killBug的执行上下文中变量对象,而当前killBug这个变量对象就是AO,因为代码执行到此killBug函数,killBug他是活跃的,他的变量对象就是AO了

简而言之,当前调用的函数他的变量对象就是AO。这是我对AO的理解,有什么不对的欢迎指出。

2,确定了this

这里主要根据调用方式确定当前this指向是什么,全局上下文this指向全局对象,局部上下文中的this不同情况指向不同地方,此处关于this指向不做深入,我们需要知道this的指向是在这个时候确定的就可以了。 箭头函数的this指向父执行上下文的this

3,创建作用域链

前面说到了作用域是一种抽象的规则与范围,确定了如何查找变量与代码访问权限,那么这个规则到底是什么。

我们知道静态作用域是在函数创建时就确定(注意不是在函数调用时候确定,函数调用时创建的是执行上下文)。这是因为函数有一个内部属性[[scope]], 函数创建的时候会保存所有的父级变量对象进来,可以理解[[scope]]就是所有父级对象的层级链,看下面代码

function fn1() {
    function fn2() {
        
    }
}

fn1创建时,其[[scope]]是所有父级变量对象,而当前fn1的父级变量对象就是全局变量对象(Global_VO),所以:

fn1.[[scope]] : [  
    Global_VO  
]

当执行到fn1中的fn2创建时,fn2其[[scope]]是所有父级变量对象,fn2声明在fn1中,所以其所有父级变量对象应该是fn1的变量对象(fn1_VO)和全局变量对象(Global_VO):

fn2.[[scope]] : [  
	fn1_VO ,
    Global_VO  
]

上面fn2 的 [[scope]]还不是fn2的作用域链,我们说了,作用域链是保存在执行上下文中,执行上下文是在函数调用时创建的,所以当调用fn2的时候。我们创建fn2的执行上下文。
创建fn2的变量对象fn2_AO(你要想叫它fn2_VO也可以,都是创建fn2的变量对象),然后绑定fn2的this

现在我们要做的事就是创建作用域链(Scope),作用域链的创建过程就是将fn2_AO与之前在函数创建时保存的的fn2的[[scope]]合并,像下面这样

// fn2_Scope = [fn2_AO].concat(fn2.[[scope]])
fn2_Scope : [
	fn2,
    fn2.[[scope]]
]

现在 我们已经知道作用域链是如何创建的,接下来就要说说作用域内查找变量的规则,当我们函数fn内查找某一变量X,首先会在当前函数fn的激活的变量对象(fn_AO)中查找,如果没有就顺着作用域链(fn_Scope)向父级变量对象中找,找不到继续向父级的父级的变量对象中找,就这样顺着作用域链一直找到全局变量对象,这就是作用域内变量的查找规则。

总结

至此,我们知道执行上下文(Executing Context)包括三部分,创建变量对象(Variable Object),this确定,创建作用域链(Scope), 而执行栈(Executing Stack)就是存放执行上下文的地方,栈顶的执行上下文就是当前激活的执行上下文,它是变量对象也就是活动对象(Active Object),同时我们也知道JavaScript函数作用域(Lexical Environment)是静态作用域,在函数创建的时候就确定了,同时生成一个[[scope]]用来保存所有父级的变量对象,而我们执行上下文中的作用域链就是当前的活动对象+当前函数生成的[[scope]]组成的,变量查找就会顺着作用域链向上,直到全局变量对象。

参考文章:
彻底明白作用域、执行上下文
深入理解Javascript之Execution Context
JavaScript的作用域和变量对象

3108