关于执行期上下文(包含作用域链)

447 阅读7分钟

1、什么是执行期上下文

JS被解析和执行的时候所在的一个环境。

举个例子,当执行到一个函数的时候,就会进行准备工作,这里的“准备工作”,让我们用个更专业一点的说法,就叫做"执行上下文(execution context)"。

2、执行上下文的类型

JavaScript 中有三种执行上下文类型。

  • 全局执行上下文 - 这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事儿:创建一个全局的window对象(浏览器情况),并且设置this的值等于这个全局对象。一个程序中只会有一个全局上下文
  • 函数执行上下文 - 每当一个函数被调用时,都会为改函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,他会按定义的顺序执行一系列步骤。
  • eval执行上下文 - 执行在eval函数内部的代码也会有它属于自己的执行上下文。

3、执行栈

JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文

执行栈,也就是在其它编程语言中所说的“调用栈”,是一种拥有 LIFO(后进先出)数据结构的栈,被用来存储代码运行时创建的所有执行上下文。

let a = 'Hello World!';

function first() {
  console.log('Inside first function');
  second();
  console.log('Again inside first function');
}

function second() {
  console.log('Inside second function');
}

first();
console.log('Inside Global Execution Context');

简述以上执行代码:

  • 运行js脚本,把全局执行上下文压入栈底。
  • 当遇到fist()函数时,执行该函数,同时把first()函数压入栈顶,继续执行
  • 在执行fist()函数时,遇到second()函数,执行该函数,并把second()函数压入栈顶。
  • second()函数执行完毕后,从栈顶弹出,同时把控制流程指针指向下一执行上下文first()
  • 继续执行first()函数,执行完毕后,同样把控制流程指针向下一执行上下文。
  • 当js脚本执行完毕后,js引擎从当前栈中移除全局执行上下文。

4、怎么创建执行上下文?

创建执行上下文有两个阶段:1) 创建阶段 和 2) 执行阶段。

5、创建阶段

JavaScript 代码执行前,执行上下文将经历创建阶段。在创建阶段会发生三件事:

  • 变量环境(Variable object,VO)
  • 词法环境(Scope chain) --- 作用域链
  • this

变量环境

变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。

因为不同执行上下文下的变量对象稍有不同,所以我们来聊聊全局上下文下的变量对象和函数上下文下的变量对象。

  • 全局上下文

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

  • 函数上下文

    在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象。

活动对象和变量对象其实是一个东西,只是变量对象是规范上的或者说是引擎实现上的,不可在 JavaScript 环境中访问,只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,所以才叫 activation object 呐,而只有被激活的变量对象,也就是活动对象上的各种属性才能被访问。

活动对象是在进入函数上下文时刻被创建的,它通过函数的 arguments 属性初始化。arguments 属性值是 Arguments 对象。

作用域链

当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。

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

这是因为函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,你可以理解 [[scope]] 就是所有父变量对象的层级链,但是注意:[[scope]] 并不代表完整的作用域链!

举个例子:

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

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

foo.[[scope]] = {
	globalContext.VO
}
bar.[[scope]] = {
	fooContext.AO,
	globalContext.VO
}

总结一下函数执行上下文中作用域链和变量对象的创建过程:

举个例子:

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

执行过程如下:

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

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

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

ECStack = [
    checkscopeContext,
    globalContext
];

3.checkscope 函数并不立刻执行,开始做准备工作,第一步:复制函数[[scope]]属性创建作用域链

checkscopeContext = {
    Scope: checkscope.[[scope]],
}

4.第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明

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

5.第三步:将活动对象压入 checkscope 作用域链顶端

checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    },
    Scope: [AO, [[Scope]]]
}

6.准备工作做完,开始执行函数,随着函数的执行,修改 AO 的属性值

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

7.查找到 scope2 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出

ECStack = [
    globalContext
];

this

在全局执行上下文中,this 的值指向全局对象。(在浏览器中,this引用 Window 对象)。 在函数执行上下文中,this 的值取决于该函数是如何被调用的。如果它被一个引用对象调用,那么 this 会被设置成那个对象,否则 this 的值被设置为全局对象或者 undefined(在严格模式下)。

6、执行阶段

在此阶段,完成对所有这些变量的分配,最后执行代码

执行上下文的代码会分成两个阶段进行处理:分析和执行,我们也可以叫做:

  • 进入执行上下文
  • 代码执行

进入执行上下文

当进入执行上下文时,这时候还没有执行代码,

变量对象会包括:

  • 函数的所有形参 (如果是函数上下文)

由名称和对应值组成的一个变量对象的属性被创建 没有实参,属性值设为 undefined

  • 函数声明

由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建 如果变量对象已经存在相同名称的属性,则完全替换这个属性;

  • 变量声明

由名称和对应值(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: undefined,
    c: reference to function c(){},
    d: undefined
}

代码执行

在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值

还是上面的例子,当代码执行完后,这时候的 AO 是:

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