js 执行上下文

什么是执行上下文(执行上下文环境)?

先看段代码:

console.log(a);		// undefined
var a = 100;

console.log(b);		// Uncaught ReferenceError: b is not defined

console.log(foo);	// ƒ foo () {}
function foo () {}

console.log(fn);	// undefined
var fn = function () {};

console.log(this);	// Window {0: global, 1: Window,...}

变量声明前输出
输出一个未定义的变量
函数声明前输出
函数表达式声明前输出

输出this
可以看出JavaScript 引擎在执行一段可执行代码之前,会先进行准备工作(也就是对这段代码进行解析(也可以称为预处理)。这个阶段会根据可执行代码创建相应的执行上下文),这些“准备工作”。其中就包括:

  1. 变量、函数表达式——变量声明,默认赋值为undefined(变量赋值是在赋值语句执行的时候进行的);
  2. 函数声明——赋值;
  3. this——赋值;

这三种数据的准备情况我们称之为“执行上下文(execution context)”或者“执行上下文环境”。
注: 以上代码在全局环境下执行的。

可执行代码

javascript在执行一个代码段之前,都会进行这些“准备工作”来生成执行上下文。这个“代码段”其实分三种情况:

  • 全局执行代码,在执行所有代码前,解析创建全局执行上下文。
  • 函数执行代码,执行函数前,解析创建函数执行上下文。
  • eval执行代码,运行于当前执行上下文中。

执行上下文栈

执行全局代码时,会产生一个执行上下文环境,每次调用函数都又会产生执行上下文环境。当函数调用完成时,这个上下文环境以及其中的数据都会被消除,再重新回到全局上下文环境。处于活动状态的执行上下文环境只有一个。JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文。

其实这是一个压栈出栈的过程——执行上下文栈(Execution context stack,ECS)

举个例子:

function fun3() {
    console.log('fun3')
}

function fun2() {
    fun3();
}

function fun1() {
    fun2();
}

fun1();

解析如下:

  1. JavaScript 开始要解释执行代码的时候,最先遇到的就是全局代码,所以 JavaScript 引擎会先解析创建全局执行上下文,然后将全局执行上下文压栈
  2. 然后遇到函数fun1时,会先解析创建函数fun1的执行上下文,然后将它的执行上下文压栈;
  3. 在fun1中调用了fun2,创建fun2的执行上下文,将fun2的执行上下文压栈;
  4. 在fun2中调用了fun3,创建fun3的执行上下文,将fun3的执行上下文压栈;
  5. fun3函数执行之后,会将其执行上下文弹栈,弹栈后执行上下文中所有的数据都会被销毁,然后把控制权返回给之前的执行上下文fun2
  6. fun2函数执行之后,会将其执行上下文弹栈,弹栈后执行上下文中所有的数据都会被销毁,然后把控制权返回给之前的执行上下文fun1。
  7. fun1函数执行之后,会将其执行上下文弹栈,弹栈后执行上下文中所有的数据都会被销毁,然后把控制权返回给之前的执行上下文----全局执行上下文
  8. 注意,全局执行上下文会一直留在栈底,直到整个应用结束。

下图就是执行上下文环境的变化过程:
执行上下文环境的变化过程

利用伪代码表示如下:
为了模拟执行上下文栈的行为,让我们定义执行上下文栈是一个数组:

ECStack = [];

程序结束之前, ECStack 最底部永远有个 globalContext:

ECStack = [
    globalContext
];

模拟压栈出栈的过程:

// fun1()
ECStack.push(<fun1> functionContext);

// fun1中调用了fun2,创建fun2的执行上下文
ECStack.push(<fun2> functionContext);

// fun2中调用了fun3,创建fun3的执行上下文
ECStack.push(<fun3> functionContext);

// fun3执行完毕
ECStack.pop();

// fun2执行完毕
ECStack.pop();

// fun1执行完毕
ECStack.pop();

// javascript接着执行下面的代码,但是ECStack底层永远有个globalContext

执行上下文的组成

当 JavaScript 代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。 执行上下文定义了变量或函数有权访问的其他数据,决定了它们各自的行为。每一个执行上下文都由以下三个属性组成:

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

变量对象

变量对象(Variable object,VO)是与执行上下文相关的数据作用域,存储了在执行上下文中定义的所有变量和函数声明,保证代码执行时对变量和函数的正确访问

全局上下文

对于全局上下文来说,全局上下文中的变量对象就是全局对象

函数上下文

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

活动对象和变量对象其实是一个东西,只是处于执行上下文的不同生命周期; 变量对象是规范上的或者说是引擎实现上的,不可在 JavaScript 环境中访问,只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,变量对象(VO)转变为了活动对象(AO),而只有被激活的变量对象,也就是活动对象上的各种属性才能被访问。

执行过程

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

  1. 进入执行上下文
  2. 代码执行

进入执行上下文

当进入执行上下文时,这时候还没有执行代码,
变量对象会包括:

  1. 函数的所有形参 (如果是函数上下文)
    • 由名称和对应值组成的一个变量对象的属性被创建
    • 没有实参,属性值设为 undefined
  2. 函数声明:
    • 由名称和对应值(函数对象(function-object),指向对函数的引用)组成一个变量对象的属性被创建
    • 如果变量对象已经存在相同名称的属性,则完全替换这个属性
  3. 变量声明
    • 由名称和对应值(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"
}

总结:

  1. 全局上下文的变量对象初始化是全局对象

  2. 函数上下文的变量对象初始化只包括 Arguments 对象

  3. 在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值

  4. 在代码执行阶段,会再次修改变量对象的属性值

参考资料:
ECMAScript规范-第三版-执行上下文
https://github.com/mqyqingfeng/Blog/issues/5
https://www.cnblogs.com/wangfupeng1988/p/3977924.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值