js代码的执行原理

ECMAJavaScript版本说明

在不同的ECMAJavaScript版本中对于js执行原理,相关术语都有一些差别
但事实上,他们只是在某些概念上的描述不一样,具体的流程是一样的
这里通过ECMAJavaScript3中的概念来学习JavaScript执行原理

浏览器内核

我们的浏览器大致由两部分组成
webkit内核为例

WebCore

负责HTML的解析,布局,渲染等等工作
关于浏览器的渲染原理可以看我以下这篇文章
浏览器渲染原理

JavaScriptCore

负责JavaScript代码的解析执行
V8引擎为例

V8引擎执行原理

V8是使用C++编写的谷歌开源高性能JavaScript引擎,用于Chromenodejs
V8可以独立运行,也可以嵌入任何C++应用程序中
V8中运行的代码,会经历以下几个步骤

Created with Raphaël 2.3.0 开始 Parse AST抽象语法树 Ignition bytecode字节码 是否需要重复调用 TurboFan MachineCode优化后的机器码 是否发生改变 Deoptimization反向优化 运行 yes no yes no

Parse

在最开始,我们的JavaScript代码会先被进行词法分析(lexical analysis),将字符串转换成token序列的形式
接下来将对其进行语法分析(Parser)并生成AST抽象语法树
如果函数没有被调用的话是不会出现在AST抽象树中的
ParseV8官方文档:Parse
有关于AST抽象语法树相关的内容可以看我这一篇文章
(未动笔,未来可寄)

Ignition

Ignition是一个解释器,负责将AST抽象语法树转换为字节码
同时会收集TurboFan所需要的相关信息,例如函数的运行信息等等
如果函数只会执行一次,那么Ignition解释执行字节码
如果函数需要执行多次,就会进入TurboFan环节
IgnitionV8官方文档:Ignition

TurboFan

TurboFan是一个编译器,负责将字节码编译成机器码
TurboFan主要负责对代码的执行进行优化
如果当一个函数被调用多次时,这个函数将会被标记为热点函数
被标记为热点函数的代码块会直接由TurboFan编译为机器码执行,不再经过字节码执行
TurboFanV8官方文档:TurboFan

Deoptimization

当函数的参数类型发生了变化,如需要传入一个Number类型的数据,传进去了String类型的数据
之前优化的机器码就无法使用了,这时就会反向优化成字节码,交给Ignition执行

执行上下文

js引擎内部有一个执行上下文栈(Execution Context Stack,简称ECS),它是用于执行代码的调用栈
全局代码块会入栈一个全局执行上下文(Global Execution Context,简称GEC
而如果是函数代码块的话就会入栈一个函数执行上下文(Functional Execution Context,简称FEC
当栈顶的上下文执行完毕后该上下文就会出栈,js引擎继续执行下一个上下文

GO对象

js引擎会在代码执行前,在堆内存中创建一个全局对象:Global Object(简称GO
所有的作用域都能访问到这个对象,即所有对象的作用域链中都包含这个对象
全局对象中包含许多属性,如setTimeOutNumberString等等
其中有一个window属性指向自身

FO对象

全局代码会在堆内存中创建一个GO
当遇到函数代码时js引擎也会创建一个函数对象(Function Object,简称FO
FO中同样存放着一些属性,如namelength等等
还存放着函数体的代码
以及作用域链[[scopes]]

VO对象

无论是全局执行上下文还是函数执行上下文都需要关联一个变量对象(Variable Object,简称VO
VO用于存放变量和函数的声明
VO不一定是新创建的对象
全局执行上下文中的VO就是GO
而在函数执行上下文中的VO就需要重新创建

AO对象

因为每个执行上下文都需要关联一个VO
全局执行上下文关联的VOGO
函数执行上下文就需要创建一个AO(Activation Object)来关联到VO
AO会使用arguments作为初始化,并且值为传入的参数
arguments为一个对象列表
AO会用来存放变量的初始化

作用域提升

无论是全局执行上下文还是函数执行上下文
在代码执行前都会将定义的变量,函数等加入到对应的VO中
但此时并不会赋值
如果当函数与变量重名时变量会被保留
这个过程被称之为作用域提升

作用域和作用域链

当进入到一个执行上下文中时,执行上下文会关联一个作用域链(Scope Chain,简称SC
SC是一个对象列表,里面存放一系列的对象
当需要查找值时如果本级作用域查找不到就会按照SC中的顺序来查找
SC和VO一样不一定是新创建的对象
全局执行上下文SC就是本身(this
函数执行上下文SC则关联到了其对应FO中的[[scopes]]属性

演示

以这一段代码来演示

        var a = 0;
        var b = 1;
        function foo() {
            console.log(a)
            var a = 2;
        }
        function bar(age) {
            function baz() {
                console.log(age)
            }
            return baz
        }
        foo()
        var func = bar(18)
        func()

初始化

此时代码还没运行,js引擎开始初始化,如生成GOFO等等

第一步

开始执行全局代码

执行到第二行,此时ab均被赋值

第二步

执行函数

开始调用foo函数,创建一个FEC和一个AO

第三步
接下来会运行foo里的代码
第一行为输出a的值,因为此时a还尚未赋值,所以控制台会输出undefined,第二行进行赋值操作

第四步
运行完毕后,FEC会出栈,此时GEC重新成为栈顶,继续执行全局代码
相关AO内存也会因为js的垃圾回收机制被回收
关于js的垃圾回收机制可以看我这篇文章
闭包与垃圾回收

第五步
此时开始执行bar函数,并将函数的返回值赋给func

第六步
bar函数执行完后,FEC会出栈,其对应的AO也本应该被回收,但因为这里形成了闭包,所以js的垃圾回收机制无法回收这部分内存
关于js闭包相关内容可以看我这篇文章
(未动笔,未来可寄)

第七步
最后执行func函数,因为barAO对象没有被回收,所以baz能访问到bar中的age

第八步
最后看看控制台的打印结果

结果

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值