JS代码执行过程

192 阅读6分钟

1.原理

解析 + 执行

1.解析

词法分析 -> 语法分析 -> 生成AST -> 作用域确定 -> 生成机器代码

  • 词法分析:
  • 语法/解析分析:生成AST
  • 确定作用域关系:通过词法作用域规则,确定变量的作用域关系。
  • 代码生成:生成浏览器js引擎能解析的底层代码。

2.执行

在js引擎中执行代码分为:分析阶段执行阶段

  1. 分析阶段
  2. 执行阶段

分析阶段

  1. 创建分析对象: 用于处理执行时,访问变量和方法时候,根据一定规则进行作用域的访问。
  2. 预编译:提高运行时的效率,会把把代码进行预编译,如变量提升。

创建分析对象

  1. VO(Variable Object)对象:该对象保存了当前执行上下文中的变量和函数地址(也就是当前作用域)。
  2. ScopeChain作用域链:VO(当前作用域) + ParentScope(父级作用域)
  3. this的指向: 可能是window,可能是调用的对象。
//伪代码
{
    VO: {
        变量:{},
        函数地址:{};
    },
    scopeChain: [VO(当前作用域), ParentScope(父级作用域)]
    this: window | object(调用的对象引用) 
}

代码预编译

console.log(num1)
var num1 = 20 

会被预编译成这样

var num1;
console.log(num1)
num1 = 20 

执行阶段

  1. 创建执行上下文:根据分析的对象创建执行上下文
  2. 上下文入栈执行
  3. 变量赋值
  4. 方法调用

2.执行上下文

定义

执行上下文(Execution context)

遇到函数执行的时候,就会创建一个执行上下文。执行上下文是当前 js 代码被解析和执行时所在环境的抽象概念。

分类

  1. 全局执行上下文(GEC):这是默认的上下文,任何不在函数内部的代码都在全局上下文中。由浏览器主动调用根函数,包含全局的window对象和this指向这个window ,一个程序中只会有一个全局执行上下文。
  2. 函数执行上下文(FEC):在函数执行的时候,都会动态创建的执行上下文,跟函数作用域类似。
  3. Eval 函数执行上下文: 执行在 eval 函数内部的代码也会有它属于自己的执行上下文。

执行上下文栈

执行上下文栈 (Execution context stack),调用栈,执行栈。 用来维护执行代码时候创建的上下文,是栈的数据结构,后进先出。

执行流程

  1. 在执行script代码时,由浏览器主动调用根函数G,创建默认全局执行上下文G,并然后押入栈顶。
  2. 根函数里,调用方法a,创建的函数执行上下文a,并把a押入栈顶,在G的上面。
  3. a函数里,调用方法b,创建的函数执行上下文b,并把b押入栈顶,在a的上面。
  4. 方法a调用完毕,同时执行上下文a也出栈。
  5. 方法b调用完毕,同时执行上下文b也出栈。
  6. 根函数G也执行完毕,全局上下文G也出栈。
//根路径默认会创建一个全局的上下执行栈

function a (){
    b() //函数调用时,创建b的上下文
}
function b (){ 

}
a()  //函数调用时,创建a的上下文

image.png

3.对象的概念 VO GO AO

  • VO(Variable Object) : 保存当前指向上下文的 变量和函数引用地址
  • GO(Global Object) :全局执行上下文的VO对象 指向 GO对象
  • AO(ACtive Object) :函数执行上下文的VO对象 指向 AO对象

4.上下文的概念 EC ECS GEC FEC

  • EC(Execution Context) : 执行上下文
  • ECS(Execution Context Stack) :执行上下文栈
  • GEC(Global Execution Context) :全局执行上下文
  • FEC(Function Execution Context) :函数执行上下文

5.实例1

var name = 'xxxx'
console.log(name)
console.log(num1)
var num1 = 20
var num2 = 100
var sum = num1 + num2
console.log(sum) 

1.分析阶段:

  1. 创建全局执行上下文,
  2. 同时创建全局GO对象,并且把全局定义的变量和方法地址都放到GO中。
  3. 把当前的 console.log(num1)num1 进行变量提升 最终全局上下文为:
//伪代码
//全局对象
GO: {
     //自带的方法和属性
       setTimeout: f(),    
       windowGlobalObject,
       alert:f(),
       ...
    //全局定义合并进来的
       name: undefined,
       num1: undefined,
       num2: undefined,
       sum: undefined,
    },

//全局上下文的分析对象
{
    VO:  GO,//由于是全局上下文 VO直接指向 GO
    scopeChain: VO //VO就是GO 就是最顶级了
    this: window 
}

分析后的预编译代码如下

var name = 'xxxx'
console.log(name)
var num1
console.log(num1)
var num1 = 20
var num2 = 100
var sum = num1 + num2
console.log(sum) 

2.执行代码

  1. 把全局上下文押入执行上下文栈中,并开始执行
  2. var name = 'xxxx' 在VO里面找GO,发现GO有变量name,对name进行赋值
  3. console.log(name) 在VO里面找GO,发现GO有变量name,打印name的值为xxx
  4. var num1 在分析阶段进行变量提升,这是预编译后的生成代码,在调用前做了声明,但未赋值。
  5. console.log(num1) , 在VO里面找GO,发现GO有变量num1,打印name的值为undefined
  6. var num1 = 20 在VO里面找GO,发现GO有变量num1,对num1进行赋值
  7. var num2 = 100 在VO里面找GO,发现GO有变量num2,对num2进行赋值
  8. var sum = num1 + num2,在VO里面找GO,发现GO有变量num1,num2,sum,对num1和num2进行运算,并赋值给sum
  9. console.log(sum) , 在VO里面找GO,发现GO有变量sum,打印sum的值为120
  10. 全局上下文出栈

4.实例2

var name = 'xxxx'
function foo() {
    var num = 20
    console.log(name)
}
foo() 

1.分析阶段:

  1. 创建全局执行上下文的分析对象,
  2. 同时创建全局GO对象,并且把全局定义的变量name和foo地址都放到GO中。
//伪代码
//全局对象
GO: {
     //自带的方法和属性
       setTimeout: f(),    
       windowGlobalObject,
       alert:f(),
       ...
    //全局定义合并进来的
       name: undefined,
       foo: fn引用地址,
    },

//全局上下文的分析对象
{
    VO:  GO,
    scopeChain: VO 
    this: window 
}
  1. foo方法分析
  2. 创建函数执行上下文的分析对象,
  3. 同时创建AO对象,并且把函数内定义的name放到AO中。
//伪代码
//AO对象
AO: {  
       num: undefined, 
       arguments: //是一个数组,这里保存的也是地址 
    },
//函数上下文的分析对象
{
    VO:  AO,
    scopeChain: [VO,parentScope] // 这里的 parentScope等于 GO
    this: window 
}

2.执行代码

  1. 根据全局分析对象,创建全局上下文。
  2. 把全局上下文押入执行上下文栈中,并开始执行
  3. var name = 'xxxx',在VO中找到GO对象,发现有name属性
  4. 对变量name进行赋值
  5. 发现是foo()方法调用
  6. 开始对foo方法进行执行。
  7. 根据foo的分析对象,创建foo函数上下文。
  8. 开始调用foo方法
var name = 'xxxx'
function foo() {
    var num = 20
    console.log(name)
}
foo() 
  1. var num = 20 先在VO中找到AO对象,并且有num,进行赋值20。
  2. console.log(name) 先在VO里面找,即AO对象,没有找到。
  3. 则在作用域scopeChain找到父作用GO,GO里面包含name属性,打印name
  4. foo函数执行完毕,函数上下文出栈。
  5. 全部代码执行完毕,全局上下文也出栈。