# JS执行过程

# 执行堆栈

当Javascript代码执行的时候会将不同的变量存于内存中的不同位置:堆(heap)和栈(stack)中来加以区分。其中,堆里存放着一些对象。而栈中则存放着一些基础类型变量以及对象的指针。

JS在执行可执行的脚本时,首先会创建一个全局可执行上下文的 globalContext,每当执行到一个函数调用时,都会创建一个可执行上下文 (execution contex) EC. 程序执行可能会存在很多函数调用,那么就会创建很多 EC, 所以 JavaScript 引擎创建了执行上下文栈 (Execution context stack,ECS) 来管理执行上下文。 当函数调用完成, js 会退出这个执行环境并把这个执行环境销毁,回到上一个方法的执行环境 ... 这个过程反复执行,直到执行栈中的代码全部执行完毕。

# 执行栈

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

# 执行栈压栈顺序

一开始执行代码的时候,变确定了一个全局执行上下文 global execution context 作为默认值。 如果在你的全局环境中,调用了其他的函数,程序将会再创建一个新的 EC,然后将此 EC 推入到栈中 execution stack

如果函数内再调用其他函数,相同的步骤将会再次发生:创建一个新的EC->把EC推入执行栈。一旦一个EC执行完成,变成从执行栈中推出 (pop). ESP 指针负责ES出栈指向

ECStack=[
    globalContext
]
function f1() {
    f2();
    console.log(1);
};

function f2() {
    f3();
    console.log(2);
};

function f3() {
    console.log(3);
};

f1();//3 2 1

Stack

# 变量对象 (Variable Object)

变量对象 VO 是与执行上下文相关的特殊对象,用来存储上下文的函数声明,函数 形参变量

var a = 10

function test(x){
    var b = 20
}
test(30)

那么他们在VO中是如何表示的呢

// 全局上下文的变量对象
VO(globalContext) = {
  a: 10,
  test: <reference to function>
};
// test函数上下文的变量对象
VO(test functionContext) = {
    x: 30,
    b: 20
 };

VO分为 全局上下文的变量对象VO,函数上下文的变量对象VO

VO(globalContext) === global

# 活动对象 (Activation Object)

在函数上下文中,变量对象被表示为活动对象 AO,当函数被调用后,这个特殊的活动对象就被创建了。它包含普通参数与特殊参数对象 (具有索引数学的参数映射表)。活动对象在函数上下文中作为变量对象使用,预编译在此阶段发生

  1. 在函数执行上下文中,VO 是不能直接访问的,此时由活动对象扮演VO的角色
  2. Arguments对象它包括如下属性:callee 、length
  3. 内部定义的函数
  4. 绑定上对应的变量环境
  5. 内部定义的变量

VO(functionContext) === AO

function test(a, b) {
    var c = 10;
    function d() {}
    var e = function _e() {}; 
    (function x() {});
}
test(10)

当进入带有参数 10 的test函数上下文时, AO 表现如下:

AO(test) = {
    a: 10,
    b: undefined,
    c: undefined,
    d: <reference to FunctionDeclaration "d"> 
    e: undefined
};

AO里并不包含函数“x”。这是因为“x” 是一个函数表达式(FunctionExpression, 缩写为 FE) 而不是 函数声明,函数表达式不会影响VO

活动对象和变量对象的区别:

  1. 变量对象(VO)是规范上或者是JS引擎上实现的,并不能在JS环境中直接访问。

  2. 当进入到一个执行上下文后,这个变量对象才会被激活,所以叫活动对象(AO),这时候活动对象上的各种属性才能被访问。

# 深度活动对象

深度活动对象 Activation Object 分为 创建阶段执行阶段,

# 创建阶段

很明显,这个时候还没有执行代码。 此时的变量对象会包括:

  1. 函数的所有形参 (only函数上下文):没有实参,属性值设为 undefined

  2. 函数声明:如果变量对象已经存在相同名称的属性,则完全替换这个属性。

  3. 变量声明:如果变量名称跟已经声明的形参或函数相同,则变量声明不会干扰已经存在的这类属性

function foo(i) {
    var a = 'hello';
    var b = function privateB() { };
    function c() {
    }
}
foo(22);

我们在执行 foo(22)的时候,EC 创建节点会类似生成下面这样的对象:

fooExecutionContext = {
    scopeChain: { Scope },
    AO: {
        arguments: {
            0: 22,
            length: 1
        },
        i: 22,

        c: pointer to function c()
        a: undefined,
        b: undefined
    },
    VO:{..}
    Scope: [AO, globalContext.VO],
}

形参 arguments 这时候已经有赋值了,但是变量还是 undefined,只是初始化的值

# 执行阶段

这个阶段会顺序执行代码,修改变量对象的值,代码一行一行的被解释执行

fooExecutionContext = {
    scopeChain: { ... },
    AO: {
        arguments: {
        0: 22,
        length: 1
    },
        i: 22,
        c: pointer to function c()
        a: 'hello',
        b: pointer to function privateB()
    },
    VO:{..}
    Scope: [AO, globalContext.VO],
    this: { 运行时确认 }
}

# 补充活动对象

var x = 10; function foo() {
    var barFn = Function('alert(x); alert(y);');
    barFn(); // 10, "y" is not defined 
}
foo();
  1. 通过函构造函数创建的函数的[[scope]]属性总是唯一的全局对象(LexicalEnvironment)。

  2. Eval code - eval 函数包含的代码块也有同样的效果

# 小结

VO 函数是上下文的链接 AO 是函数自身的

ECStack = [
    fun3
    fun2,
    fun1,
    globalContext
];

VO分为 全局上下文的变量对象VO,函数上下文的变量对象VO

VO(globalContext) === global

VO(fooExecutionContext) === function

# 异步代码

当一个异步代码(如发送ajax请求数据)执行后会如何呢?这需要了解另一个概率就是:事件队列(Task Queue)

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

function test() { 
    var result = []
    for (var i = 0; i < 10; i++) {
        result[i] = function() {
            return i
        }
    }
    return result
}
let r = test()
r.forEach(fn => {
    console.log('fn',fn()) 
})
  1. 函数test执行完出栈,留下 AO(test)有个i的指向
  2. 函数在执行的时候,函数的执行环境才会生成. 所以fn 执行的时候生成作用域链条指向如下

AO(result[i]) --> AO(fn) --> VO(G) 加个闭包就立⻢创建了执行环境

那么其实一切也就迎刃而解了。闭包的原理是 Scope (堆空间中存储closure(foo)), this 的原理是动态绑定,作用域链 的原理是Scope: [AO, globalContext.VO], eval 不能回收的原理是推不进 AO ,变量提升的原理是AO的准备阶段,异步 队列的原理是 ECS.

# ES5+

JS执行上下文的创建阶段主要负责三件事:

  1. 确定this

  2. 创建词法环境 (LexicalEnvironment)

  3. 创建变量环境 (VariableEnvironment)

创建过程如下:

ExecutionContext = {  
    // 确定this的值
    ThisBinding = <this value>,
    // 创建词法环境
    LexicalEnvironment = {},
    // 创建变量环境
    VariableEnvironment = {},
};

# 确定 this

官方的称呼为 This Binding,在全局执行上下文中,this 总是指向全局对象,例如浏览器环境下 this 指向 window 对象。而在 nodejs 中指向这个文件的 module 对象。

在函数执行上下文中,this的值取决于函数的调用方式,this 的值取决于函数的调用方式。具体有:默认绑定、隐式绑定、显式绑定(硬绑定)、new绑定、箭头函数

# 词法环境

词法环境有两部分组成:

  1. 词法记录:用于存储变量和函数声明的实际位置

  2. 对外部环境引入记录:用于保存它可以访问的其它外部环境 (有点作用域链的意思)

前面提到全局执行上下文和函数执行上下文,所以导致了词法环境也分两种:

全局词法环境

是一个没有外部环境的词法环境,其外部环境引用为 null。拥有一个全局对象(window 对象)及其关联的方法和属性(例如数组方法)以及任何用户自定义的全局变量 this 的值指向这个全局对象

// 全局执行上下文
GlobalExectionContext = {  
    // 词法环境
  LexicalEnvironment: {    	  
      // 环境记录
    EnvironmentRecord: {   
        // 全局环境		
      Type: "Object",      		   
      // ... 标识符绑定在这里 
      // 对外部环境的引用
      outer: <null>  	   		   
  }  
}

函数词法环境

用户在函数中定义的变量被存储在环境记录中,包含了 arguments 对象。对外部环境的引用可以是全局环境,也可以是包含内部函数的外部函数环境。

// 函数执行上下文
FunctionExectionContext = { 
  LexicalEnvironment: {  	  
    EnvironmentRecord: {  
      // // 函数环境		
      Type: "Declarative",  	   
      // ... 标识符绑定在这里 			
      // 对外部环境的引用
      outer: <Global or outer function environment reference>  
  }  
}

# 变量环境

为了继续去适配早期的JS的var等,新的规范增加了变量环境(VariableEnvironment)。变量环境也可以说是词法环境,它具备词法环境所有的属性。其环境记录器包含了由变量声明语句

ES6 中的区别在于词法环境用于存储函数声明与 let const 声明的变量 ,而变量环境仅仅存储 var 声明的变量。

通过一串伪代码来理解它们:

let a = 20;  
const b = 30;  
var c;

function multiply(e, f) {  
 var g = 20;  
 return e * f * g;  
}

c = multiply(20, 30);

执行上下文如下所示:

GlobalExectionContext = {

  ThisBinding: <Global Object>,

  LexicalEnvironment: {  
    EnvironmentRecord: {  
      Type: "Object",  
      // 标识符绑定在这里  
      a: < uninitialized >,  
      b: < uninitialized >,  
      multiply: < func >  
    }  
    outer: <null>  
  },

  VariableEnvironment: {  
    EnvironmentRecord: {  
      Type: "Object",  
      // 标识符绑定在这里  
      c: undefined,  
    }  
    outer: <null>  
  }  
}

FunctionExectionContext = {  
   
  ThisBinding: <Global Object>,

  LexicalEnvironment: {  
    EnvironmentRecord: {  
      Type: "Declarative",  
      // 标识符绑定在这里  
      Arguments: {0: 20, 1: 30, length: 2},  
    },  
    outer: <GlobalLexicalEnvironment>  
  },

  VariableEnvironment: {  
    EnvironmentRecord: {  
      Type: "Declarative",  
      // 标识符绑定在这里  
      g: undefined  
    },  
    outer: <GlobalLexicalEnvironment>  
  }  
}

注意: 只有在遇到函数multiply调用时才会创建函数执行上下文

在执行上下文创建阶段,函数声明 与 var 声明的变量在创建阶段已经被赋予了一个值, var 声明被设置为了 undefined,函数被设置为了自身函数,而 let const 被设置为 uninitialized(未初始化)

这就是为什么你可以在声明之前访问 var 定义的变量(尽管是 undefined),但如果在声明之前访问 letconst 定义的变量就会提示引用错误的原因,这就是所谓的 变量提升

# 参考

通过动图了解JS中的ECStack、EC、VO 和 AO

更新时间: 12/9/2020, 9:34:18 PM