如何理解执行上下文?

120 阅读3分钟

执行上下文

当 JavaScript 代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。

对于每个执行上下文,都有三个重要属性:

  • 变量对象(Variable object,VO)
  • 作用域链(Scope chain)
  • this

深入变量对象

变量对象与实行上下文相关的数据作用域。执行上下文分为全局上下文和函数上下文。变量对象在两个上下文稍有不同。

全局上下文

全局上下文中的变量对象就是全局对象。

函数上下文

函数上下文的代码会分为两个阶段(分析,执行)进行处理,在这两个阶段中变量对象怎么被创建出来呢?

分析阶段,这个时候进入了函数上下文。

变量对象包括函数的所有形参,函数的声明和变量的声明(如果变量的声明与函数重名,那么函数优先)

举个例子

function foo(a) {
  var b = 2;
  function c() {}
  var d = function() {};b = 3;
​
}
foo(1);

进入分析阶段,这时候的变量对象(活动对象)是这样的:

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: undefined,
    c: reference to function c(){},
    d: undefined
}

可以发现,arguments先被初始化,形参也被初始化了。上下文会添加函数声明(reference to function c(){}),变量声明(undefined)的初始值

执行阶段,也就是顺序执行代码,修改变量对象的值

还是原来的例子:

function foo(a) {
  var b = 2;
  function c() {}
  var d = function() {};b = 3;
​
}
foo(1);

执行后的对象变量(活动变量,AO)为

AO = {
    arguments: {
        0: 1,
        length: 1
    },
    a: 1,
    b: 3,
    c: reference to function c(){},
    d: reference to FunctionExpression "d"
}

可以发现,形参,函数声明,变量声明的值都修改

变量对象的创建过程

  1. 全局上下文的变量对象初始化是全局对象
  2. 函数上下文的变量对象初始化只包括 Arguments 对象
  3. 在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值
  4. 在代码执行阶段,会再次修改变量对象的属性值

深入作用域链

什么是作用域链?

深入变量对象中提到,查找变量的过程中,会从当前上下文的变量对象中去查找。如果没有就从父级的执行上下文的变量对象去查找。这个由执行上下文的变量对象构成的链表叫做作用域链

函数的作用域在函数定义的时候就决定了

因为函数有一个内部属性[[scope]],当函数创建的时候,就会保存在所有的父变量对象到其中。

举个例子

function foo() {
    function bar() {
        ...
    }
}

函数创建的时候,各自的[[scope]]

foo.[[scope]]=[
​
•   globalContext.VO
​
]
​
bar.[[scope]]=[
​
•   fooContext.AO,
​
•   globalContext.VO
​
]

当函数激活(执行函数时),进入函数上下文,创建了VO/AO后,就会将活动对象(变量对象)放在作用域链的前端

Scope = [AO].concat([[scope]])

变量对象与作用域链在函数执行中的流程

var scope = "global scope";
function checkscope(){
    var scope2 = 'local scope';
    return scope2;
}
checkscope();

1.checkscope函数被创建,保存作用域到内部属性[[scope]]

checkscope.[[scope]]=[
​
•   globalContext.VO
​
];

2.执行checkscope函数,创建checkscope函数执行上下文,此函数执行上下文被压入执行上下文栈

ECStack = [
​
•   checkscopeContext,
​
•   globalContext
​
]

3.执行函数前,进入函数上下文的分析阶段

4.复制函数[[scope]]属性创建作用域链

checkscopeContext ={
​
Scope: checkscope.[[scope]];
​
}

5.用auguments创建活动对象,随后初始化活动对象,加入形参,函数声明,变量声明

checkscopeContext ={
​
AO:{arguments:{length:0
​
},
​
scope2:undefined
​
}
​
Scope: checkscope.[[scope]];
​
}

6.将活动对象AO压入作用域链的最前端

checkscopeContext ={
​
AO:{arguments:{length:0
​
},
​
scope2:undefined
​
}
​
Scope: [AO,checkscope.[[scope]]];
​
}

7.分析阶段结束,进入执行上下文执行阶段

8.随着函数的执行,修改AO活动对象的属性值

checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: 'local scope'
    },
    Scope: [AO, [[Scope]]]
}

9.查找到scope2,返回scope的值即可,函数执行完毕,checkscope函数上下文从执行上下文栈弹出

ECScope =[
​
globalContext
​
];