lznbuild / my-blog

自己的博客
9 stars 1 forks source link

JavaScript执行上下文 #7

Open lznbuild opened 4 years ago

lznbuild commented 4 years ago

网上关于执行上下文的总结大部分都停留在 ES5 甚至 ES3 的规范里的内容,后期如果有时间,会补充一篇 TC39 发布的最新的执行上下文的总结。

什么是执行上下文

执行上下文是当前 JavaScript 代码被解析和执行时所在环境的抽象概念

执行上下文定义了变量或函数是否有权访问的其它数据,决定了他们各自的行为。

执行上下文的类型

执行栈

执行栈,也叫调用栈,具有 LIFO(后进先出)结构,用于存储在代码执行期间创建的所有执行上下文。

首次运行 JS 代码时,会创建一个全局执行上下文并Push到当前的执行栈中。每当发生函数调用,引擎都会为该函数创建一个新的函数执行上下文并Push到当前执行栈的栈顶。

根据执行栈LIFO规则,当栈顶函数运行完成后,其对应的函数执行上下文将会从执行栈中Pop出,上下文控制权将移到当前执行栈的下一个执行上下文。

下面举个一个栗子

var a = 'Hello World!';

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

function second() {  
  debugger; // 可以通过断点在chrome 开发者工具中查看执行上下文栈
  console.log('Inside second function');  
}

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

通过断点,查看执行上下文栈

console.trace()方法可以追踪执行栈的变化。

const a = ()=> {
    console.log('a');
    b()
  }

  const b = ()=> {
    console.log('b');
    console.trace()
    c()
  }

  const c = ()=> {
    console.log('c');
  }

  a()

执行上下文的生命周期

创建阶段

( 以下内容是ES3标准中的 )

执行上下文创建过程中,需要做以下几件事:

  1. 创建变量对象(variable object):首先初始化函数的参数arguments,提升函数声明和变量声明到变量对象。

  2. 创建作用域链(Scope Chain):在执行期上下文的创建阶段,作用域链是在变量对象之后创建的。(作用域负责收集和维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限,后面会对作用域详细解读)

  3. 确定this的值。

把上面的内容简化一下就是,对于每个执行上下文,都有三个重要属性:

this和作用域链这两个很常见,下面解释一下变量对象。

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

可以这样理解,全局变量会定义在window上(var声明),同样函数中的局部变量也应该定义在一个对象上,只是这个对象不可访问而已。

在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象。函数调用时,VO被激活成了AO,也就是变量对象变成了活动对象。

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

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

垃圾回收器会标记活动对象和非活动对象,个人感觉应该和这部分知识是有关的。( 待验证 )

执行阶段

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

  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. 在代码执行阶段,会再次修改变量对象的属性值

最后让我们看个例子:

function foo() {
    console.log(a);
    a = 1;
}

foo(); // ???

function bar() {
    a = 1;
    console.log(a);
}
bar(); // ???

第一段会报错:Uncaught ReferenceError: a is not defined。

第二段会打印:1。

这是因为函数中的 "a" 并没有通过 var 关键字声明,所有不会被存放在 AO 中。

第一段执行 console 的时候, AO 的值是:

AO = {
    arguments: {
        length: 0
    }
}

没有 a 的值,然后就会到全局去找,全局也没有,所以会报错。

当第二段执行 console 的时候,全局对象已经被赋予了 a 属性,这时候就可以从全局找到 a 的值,所以会打印 1。

至此,执行上下文中的变量对象就总结完了,下篇讲解作用域。

再强调一次,以上内容都是ES3的总结,后面的标准中关于执行上下文的改动很大。

在 ES5 中,改进了命名方式,把执行上下文最初的三个部分改为下面这个样子。

通过 let 声明的变量,在编译阶段会被存放到词法环境。
通过 var 声明的变量,在编译阶段全都被存放到变量环境里面。

在 ES2018 中,执行上下文又变成了这个样子,this 值被归入 lexical environment,但是增加了不少内容。

后面有时间会补充一篇最新标准中的执行上下文这部分的文章。