本文主要是介绍js执行过程中的执行环境相关内容,不涉及V8相关知识。

一、基本概念

当Javascript代码执行的时候会将不同的变量存储到内存的不同位置:堆(heap)栈(stack)中来进行区分。其中堆存放着一些对象;栈中存放着一些基础类型变量以及对象的指针。我们常说的执行栈和上面所说的栈的意义有些不同。

Javascript在执行可执行脚本时,首先会创建一个全局执行上下文(globalContext),每当执行一个函数调用时都会创建一个可执行上下文(execution context)EC。当然可执行程序可能会存在很多函数调用,那么就会创建很多EC,所以Javascript引擎创建了执行上下文栈(Execution context stack)ECS来进行管理执行上下文。当前函数执行完成后,js会退出这个执行环境并把这个执行环境销毁,回到上一个方法的执行环境中,这个过程是反复执行的,直到执行上下文栈中的代码全部执行完成。
  • 执行上下文栈:ECS
  • 全局对象:GO(global Context)
  • 活动对象:AO
  • 变量对象:VO
  • 全局执行上下文:GC
  • 可执行上下文:EC
  • 函数执行栈:callback stack,这个就是执行栈ECS的可视化

    二、执行上下文栈

    浏览器解释器执行js程序是单线程的,这就意味着同一时间只能有一个事情在进行。其他的活动和事件只能排队等候,生成一个等候队列,这就是执行栈(ESC)。

    1、执行上下文栈压栈顺序

    一开始执行代码时,就会确定一个全局执行上下文(global execution context)作为栈底的默认值,这个全局执行上下文是永远不会被执行上下文栈弹出的。如果在全局环境中,调用了其他函数,就会重新创建一个新的EC,然后将这个可执行上下文推入执行上下文栈顶。

    如果函数内调用了其他函数,相同的步骤就会发生:创建一个新的可执行上下文(EC),将新EC推入执行上下文栈顶。一旦一个可执行上下文(EC)执行完成,就会从执行上下文栈中推出(pop)。ESP指针负责EC出栈的指向问题。
    1. ECSstack = [ // 执行上下文栈
    2. globalContext // 全局上下文
    3. ]

    备注:被压入到执行上下文栈中的全局执行上下文,它的实质就是全局对象(GO)。创建全局对象时,会将一些Javascript一些对象作为它的属性;并且会将window对象进行挂载,并将window对象的指向自身。

    当多个函数存在,并且在函数内调用其他函数的执行上下文栈的压栈过程:
    1. function fun3() {
    2. console.log('fun3');
    3. }
    4. function fun2() {
    5. fun3();
    6. }
    7. function fun1() {
    8. fun2();
    9. }
    10. fun1();
    11. // 执行上下文栈的结果如下
    12. ECSstack = [
    13. fun3,
    14. fun2,
    15. fun1,
    16. globalContext
    17. ]

    我们知道执行上下文栈中存放着全局上下文,全局上下文中存放着全局对象。那么可执行上下文(EC)被推入执行上下文栈中时,可执行上下文(EC)中存放的就是活动对象(AO)和变量对象(VO),实质上函数执行被推入执行上下文栈中的是当前函数的活动对象(AO)和变量对象(VO)。

    活动对象(AO)管理着函数内部的变量;变量对象(VO)负责承接外部的联系,这样就形成了作用域链。

    三、变量对象

    变量对象(Variable Object)VO是与执行上下文相关的特殊对象,它用来存储当前上下文的函数声明、函数的行参和变量。

    通过一下例子来理解一下变量对象:
    1. var a = 10;
    2. function test(x) {
    3. var b = 20;
    4. }
    5. test(30);
    6. // 全局执行上下文的变量对象(VO)
    7. VO===AO(global execution context) {
    8. a: 10,
    9. test: <reference to function>
    10. }
    11. // test函数的执行上下文中的变量对象(VO)
    12. VO(test functionContext) {
    13. x: undefined,
    14. b: undefined
    15. }

    从上面的例子中可以得到:
  • 只有全局执行上下文和可执行上下文(EC)存在变量对象(VO)

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

    四、活动对象

    在函数可执行上下文中,变量对象(VO)表示为活动对象(AO),当函数被调用时,这个特殊对象被创建。它包含普通参数与特殊参数对象。活动对象在函数上下文中作为变量对象使用。
    1. function test(a, b) {
    2. var c = 22;
    3. function d() {}
    4. var e = function _e() {}
    5. (function() {});
    6. }
    7. test(10);
    8. AO(test) = {
    9. arguments: {
    10. 0: 10,
    11. 1: undefined,
    12. length: 2
    13. },
    14. a: 10,
    15. b: undefined,
    16. c: 22,
    17. d: pointer to funciton(),
    18. e: pointer to funciton()
    19. }

    通过以上例子可知:
  • 闭包函数是不会挂载到当前可执行上下文的AO或者VO上;

  • 在函数可执行上下文中,VO是不能被访问的,此时有活动对象AO来扮演VO的角色;
  • AO包含:Arguments对象、内部定义的函数、内部定义的变量、绑定上对应的环境变量。

    五、总结

    以下总结可能和上方的VO和AO解释不同,因为VO是一个不可访问的,所以第三节变量对象中的例子是最后执行完成的样子,实质上就是AO的表现形式。

    1、粗略总结:

  • Javascript引擎发现有代码调用了一个函数

  • 在执行这个函数之前会创建一个可执行上下文EC
  • 要执行的函数进入创建阶段,就是创建当前要执行函数的变量对象(VO)阶段(创建阶段)
  • 将当前可执行上下文压入执行上下文栈的栈顶
  • 进入执行阶段,创建活动对象阶段(AO)阶段(执行阶段)

    2、变量对象VO

    在Javascript引擎发现调用了一个函数,会经历创建可执行上下文的过程,创建当前执行函数的变量对象的过程,并通过scope属性指向外层的变量对象VO(其实就是通过变量对象VO指向了外层对象的活动对象AO),通过这样的指向就形成了作用域链。

    在创建变量对象VO时,会把所有的变量的声明放到一个对象属性上,但是它们的属性值为undefined,这就是变量提升的过程。简单来说,变量对象就是存储变量的,然后本代码和子代码在执行的时候能够知道变量的值。并且变量对象VO中创建顺序:参数、变量、函数。

    3、活动对象AO

    活动对象AO可以理解为变量对象VO的实例,变量对象VO为活动对象AO提供了所有变量对象的模版。

    变量对象VO和活动对象AO的关系:
  • 在创建阶段会创建变量对象VO,VO是不能被访问的,但是可以访问活动对象AO的成员

  • 变量对象VO和活动对象AO其实是一个东西,只是处在不同的可执行上下文的声明周期。VO是在创建阶段,AO是在执行阶段(就是被放入执行上下文栈的栈顶时)。粗粗暴理解就是当前可执行上下文位于执行上下文栈的栈顶的时候VO就会转换成AO
  • AO实际上包含VO的。因为除了VO的属性外,还包含arguments这个特殊的对象。活动对象AO在执行阶段被执行,处理实例化(激活)变量对象VO之外,还确定函数的arguments对象和逐行执行,将变量进行赋值

    4、例子

    根据以下代码进行分析:
    1. var a = 1;
    2. function A() {
    3. var c = 3;
    4. function B() {
    5. var b = 2;
    6. }
    7. B();
    8. }
    9. A();

    在执行以上代码时,首先会初始化全局环境:
  • 初始化全局环境,确定全局执行上下文

  • 初始化全局的变量对象VO和活动对象AO,由于全局对象只有一个,所以VO和AO一致都是GO(全局对象)

    当执行A函数时
  • 初始化创建当前可执行上下文

  • 创建当前变量对象VO,然后初始化作用域链
  • 执行阶段,根据变量对象VO创建活动对象AO,并确定arguments对象等
    1. {
    2. AExecutionContext: {
    3. AO: {
    4. argument: {
    5. length: 0
    6. },
    7. c: 3
    8. },
    9. VO: {....},
    10. this: { 运行时确定 }
    11. Scope: [ 当前的AO, 外层的AO(就是GO)]
    12. },
    13. VO===AO===GO===globalContext: {
    14. a: 1,
    15. A: <Func>,
    16. this: window,
    17. Scope: window
    18. }
    19. }
    20. // 以上就是描述一个执行上下文栈的形态
    21. // 如果A函数中找不到要使用的变量就会沿着scope找到全局执行上下文GC中(就是全局对象)
    22. // 要注意的是作用域链是词法作用域,和调用关系无关,通过词法关系产生
    23. // 也可以说就是通过外层函数的变量对象VO来产生的

    六、最后

    当一个异步代码执行时就会引入事件队列这个概念。当js引擎遇到异步事件后,其实不是一直等待着异步事件返回,而是将异步事件挂起。等到异步事件执行完毕后,会放入事件队列中(注意:此时只是异步事件执行完成,其中的回调函数并没有去执行)。当执行队列执行完毕,主线程处于闲置状态时,会去事件队列中抽取最先被推入队列中的异步事件的回调,并放入执行上下文栈中,并执行同步代码。如此反复,这样就形成了一个无限循环。这个过程被称为事件循环。