JavaScript系列:JavaScript的执行过程

802 阅读8分钟

在上一篇理解JavaScript的运行环境中,知道了JavaScript是一门高级语言,需要转化成机器指令,才能在电脑的CPU中运行。使JavaScript代码转换成机器指令,是通过JavaScript引擎来完成的。

JavaScript引擎在把JavaScript代码转换成机器指令过程中,先对JavaScript代码进行解析(词法分析,语法分析),生成AST树,然后在通过一些列操作转换成机器指令,从而在CPU中运行。

06_1.png

从上面图中,就可以看出,JavaScript代码一定需要经过解析,才能运行(后面会体现出来)。

理解JavaScript的执行过程,先来来学习几个概念吧。

执行上下文栈

执行上下文栈(Execution Context Stack,简写ECS),也被称为函数调用栈

JavaScript引擎内部实现了一个执行上文栈,目的就是为了执行代码。只要有代码执行,一定是在执行上下文栈中执行的。

从名字可以看出,这是一个栈结构,意味着可以进行入栈出栈操作,也有着栈底栈顶的概念。

全局执行上下文

全局执行上下文(Global Execution Context,简写GEC)。

只要代码想要执行,一定经过执行上下文栈,而执行上下文栈也被称为函数调用栈,也就意味着代码是以函数的形式被调用。但是全局代码(比如定义变量,定义函数等)并不是函数的形式,我们并不能主动调用代码,而被动的需要浏览器去调用代码。起到该作用的就是全局执行上下文,先解析全局代码然后执行。

全局执行上下文放在函数调用栈的栈底

函数执行上下文

函数执行上下文(Function Execution Context,简写FEC)。

在执行代码的过程中,如果遇到函数的主动调用,就会生成一个函数执行上下文,入栈到函数调用栈中;当函数调动完成之后,就会执行出栈操作。

三者大致的关系如下:

06_2.png

理解执行上下文

全局执行上下文和函数执行上下文,大致也分为两个阶段:编译阶段执行阶段

  • 编译阶段:其实就是解析代码的过程。(难点也是重点)

    解析过程中,保存了三个重要的信息:

    1. VO(Variable Object)对象:该对象保存了当前执行上下文中的变量和函数地址(也就是当前作用域)。
    2. 作用域链:VO(当前作用域) + ParentScope(父级作用域)
    3. this的指向: 视情况而定。
  • 执行阶段:依次执行代码,给变量赋值等等一些操作。

06_3.png

我的简单理解就是:编译创建信息,执行使用信息

JavaScript的执行过程分析

上面说到了太多的概念,可能会云里雾里的。那么直接看一段代码的执行过程,可以加深对上面的概念认识和理解。

 var name = 'copyer'
 var age = 18
 foo()
 function foo() {
     var city = 'cq'
     console.log(city)
 }
 console.log(message)
 console.log(age)

分析代码:

第一步:全局代码处理

变量定义(name、age),函数定义(foo),都是需要全局执行上下文来进行处理的。

全局执行上下文的两个阶段:

  • 编译阶段:在解析的过程中,会在堆内存中开辟一个地址(0x100),来保存一个GO对象; GO对象里面记录着JavaScript全局提供的一些属性和方法,和自己写的全局变量(还没有赋值)和全局函数。确定了三个信息:

    • VO:指向GO的地址
    • Scope Chain: VO(因为全局就是最顶层的作用域,就没有父级作用域)
    • this: window
  • 执行阶段:依次执行代码,对GO对象,进行变量初始化,以及对函数的调用。

认识GO对象

GO:Global Object,即全局对象,在全局执行上下文的编译阶段生成。

GO里面保存了JavaScript内部提供的一些全局的函数和属性(setTimeout,window,Math等等)。

在编译阶段,也会把自己写的全局变量添加进去(name,age);解析是函数的话,也会在堆内存中开辟一个新的地址(0x200)来保存函数信息。在GO对象中,就会以key-value的形式保存(key:函数名,value:内存地址)

 var GlobalObject = {
     //自带的
     Math: xxx,
     setTimeout: xxx,
     windowGlobalObject,
     ...
     // 新增的
     name:undefined,
     age: undefined,
     foo: 0x200
 }

函数内存空间

在解析过程中,如果遇到函数,就会在堆内存中开辟一个新的空间,来保存函数的一些信息。那么保存的有哪些内容呢?

  1. 父级作用域(parentScope): 函数的父级作用域在定义的时候就已经确定了。
  2. 函数执行体

06_6.png

第二步:执行全局代码过程中,遇到函数调用

如果在执行的过程中,遇到的函数调用,就会产生一个函数执行上下文添加到函数调用栈中(入栈);函数调用完成之后,就会弹出函数调用栈(出栈)。

函数执行上下文的两个阶段:

  • 编译阶段:在解析的函数过程中,会在堆内存中开辟一个地址(0x300),来保存一个AO对象; AO对象里面记录着函数形参(arguments)和定义的变量(包括内部函数定义)。确定了三个信息:

    • VO:指向AO的地址
    • Scope Chain: AO + parentScope(函数定义的时候,parentScope就已经确认了)
    • this: window(一般根据调用的情况而定,这里是window调用的)
  • 执行阶段:依次执行函数体中的代码,对AO对象,进行变量初始化,以及对函数的调用。执行完毕之后,就弹出函数调用栈,也会自动清空堆内存中的AO对象。

认识AO对象

AO:Activation Object,即活跃对象(当前函数处于激活的状态),在函数执行上下文的编译阶段生成。

里面保存了函数形参(arguments)和函数内部的变量

 var ActivationObject = {
     arguments: /是一个数组,这里保存的也是地址/
     city: undefined,
 }

06_7.png

JavaScript的变量提升

JavaScript中有一种现象就是变量提升。这一过程,就发生在 (全局,函数)执行上下文的编译阶段

在对JavaScript进行词法分析的时候,就会把变量保存在一个对象(GO、AO)中,只是没有赋值而已。对于函数定义而言,在解析的过程也会在堆内存中开辟一个新的地址,保存函数内容。所以,函数也能提前调用,不会报错。

 console.log(name)  // undefined
 var name = 'copyer'
 foo() // 不会报错
 function foo() {
     ...
 }

从内存的角度,真的能解释很多现象。

JavaScript的作用域链

在(全局,函数)执行上下文的编译阶段保存了三个信息,其中两个就是关于作用域的:

  • VO:指向一个对象(AO,GO),也可以简单的理解为当前作用域
  • Scope Chain: 作用域链

看下面的一段代码

 var message = 'copyer'
 function foo() {
     var age = 18
     function bar() {
         console.log(message)
     }
     bar()
 }
 foo()

代码分析

全局执行上下文:编译阶段生成GO对象(0x100)

  • VO:0x100(GO)
  • scope chain:GO

函数执行上下文(foo):编译阶段生成AO对象(0x200)

  • VO: 0x200(AO对象,foo)
  • scope chain:VO(0x200) + GO(0x100, parentScope)

函数执行上下文(bar):编译阶段生成AO对象(0x300)

  • VO: 0x300 (AO, bar)
  • scope chain: VO(0x300) + AO(0x200, parentScope)

06_8.png

在执行 bar 函数体的时候,发现message 在当前VO(0x300)中没有,就会沿着scope chain继续寻找。

  • 在 VO(0x300)中没有message,就会去parentScope(0x200)中去寻找。
  • 在 VO(0x200)中也没有message,就会继续去parentScope(0x100)中去寻找。
  • 在 VO(0x100)中发现message,也就是GO,所以打印 message 为 copyer

这就是所谓的作用域链。

记住两句话

  1. 父级作用域在编译阶段就已经确定了。
  2. 查找变量就是按照作用域链查找(找到近停止)。

总结

理解JavaScript的执行过程,过程是痛苦的。要了解的概念确实有点多,但是如果掌握了,额,,,也就掌握了。

  1. 理解执行上下文栈(函数调用栈): 所有代码执行的地方。
  2. 理解全局执行上下文: 针对全局代码。
  3. 理解函数执行上下文: 针对函数。
  4. 执行上下文的两个阶段: 编译阶段和执行阶段。
  5. 理解VO,GO,AO。

理解上面的概念之后,那么JavaScript的变量提升和作用域链也就迎刃而解了。

本篇总结的有点杂乱,如果有误的地方,请指教。