深入JavaScript运行原理
深入V8引擎原理
- 使用C++编写的,可以独立运行,也可以嵌入到任何C++应用程序中
Parse模块
-
Parse模块会将JS代码转成AST(抽象语法树),这是因为解释器并不直接认识JS代码
- 如果函数没有被调用,那么是不会转换成AST的
- 官方文档v8.dev/blog/scanne...
Ignition模块
- 是一个解释器,会将AST转成ByteCode
- 同时会收集TurboFan优化需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算)
- 如果函数只调用一次,Ignition会直接解释执行ByteCode
- 官方文档v8.dev/blog/igniti...
TurboFan模块
- 是一个编译器,可以将字节码编译为CPU可以直接执行的机器码
- 如果一个函数被多次调用,那么这个函数就会被标记成热点函数 ,就会经过 TurboFan转换成优化的机器码,提高代码的执行性能
- 但是机器码实际上也会被还原为ByteCode,这是因为如果后续函数执行过程中,类型发生了变化(比如两数相加的函数,传入了两个字符串) ,之前优化的机器码并不能正确的处理运算,就会你想的转成字节码
- 官方文档
JS执行过程
- 以下代码如何运行(先以ES6之前的语法为例子)
ini
var message = "Global Message";
function foo() {
var message = "Foo Message";
}
var num1 = 10;
var num2 = 20;
var res = num1 + num2;
console.log(res);
初始化全局对象(过程一)
- js引擎会在初始化代码之前,会在堆内存中创建一个全局对象:Global Object(GO)
- 该对象所有的作用域都可以访问
- 里面会包含Date、Array、Stringdeng
- 还有一个window属性指向自己
执行上下文(补充概念)
js引擎内部有一个执行上下文栈(Execution Context Stack 简称ECS),用于执行代码的调用栈
- 代码为了执行,会在ECS中创建执行上下文(Execution Context,简称EC),之后放到ECS中;而全局代码创建的是GEC,全局上下文
-
多个活跃的执行上下文,会在逻辑上形成栈的数据结构
- 对于这句话的理解,即在执行全局函数的时候,遇到了需要执行其他函数的代码,比如fn(),此时在ECS(上下文执行栈)中,生成一个新的EC(执行上下文),并压入栈顶
全局代码执行流程(过程二)
-
全局代码执行前,会创建一个全局执行上下文GEC ,那么GEC 放入到ECS中包含两部分
-
在代码执行前,会在V8引擎中parse 模块转成AST的过程中 ,会将全局定义的变量(会声明undefined ),函数(函数会同时被创建 )放入到VO 中,而全局的VO 就是堆内存中的GO
- VO对象(只有在正式执行代码前才会创建 ),每一个执行上下文GEC 都会关联一个VO(Variable Object),变量和函数声明都会被添加到这个对象中
- 全局的的GO会作为VO,会创建作用域链,VO,this
- 通过这个过程就可以说明,在给一个变量赋值之前,去访问它,是undefined,而访问一个函数却可以被执行
- 同时在这个过程中,有一个变量提升的过程
- 在代码执行中,对变量进行赋值,或者执行其他函数
-
函数代码执行过程(过程三)
在全局代码的执行过程中,遇到了函数的调用,那么,接下来会怎么执行呢?
- 通过上述的学习,我们知道,在执行 一段新的代码时候 ,都会在 上下文执行栈(ECS) 中创建 执行上下文(EC) ,那么遇到函数也不例外,就会创建一个FEC
- 同时在正式执行代码前,还会再堆内存中,创建一个 VO对象,用于存放函数的变量声明 等内容,针对于函数的VO,是用AO来代替的(若只是声明函数,没有调用,就不会有AO的创建)
- 在AO中,还会声明一个arguments,用于存放传入的参数,是有值的
- 接下来就会正式运行代码,将变量赋值
- 函数执行完毕之后,上下文执行栈中的 函数执行上下文 就会弹出栈,而其对应的AO,是否会销毁,就需要看后续的代码,大概率是会进行销毁的
思考,多次调用foo,以及foo中调用其他的函数,会如何执行
作用域和作用域链(变量的查找要在定义的位置开始,和在哪里调用的无关)
-
目前,最常见的就是 全局作用域和函数作用域 ,注意:声明对象用的大括号,不会生成作用域
-
当在一个作用域中使用一个变量,首先会在自己的作用域中查找是否存在这个变量,若没有的话,就会在上层作用域查找,这样就会形成一条 作用域链
-
作用域和作用域链是在函数定义的时候,就确定好的(我们可以借助浏览器的调试工具去查看)
-
接下来我们可以看三种情况
-
情况一,全局代码(具体流程见上面的内容)
- 在执行log函数的时候,会现在自己的作用域中去查找变量,没有找到就会顺着作用域链去查找
- 作用域链创建和VO在代码执行前就创建好的
-
-
情况二:全局代码中,遇见简单的函数
- 会先对函数进行声明,创建对应的函数对象
- 在正式执行代码前,会创建相应的VO对象和作用域链
- 该函数的作用域链指向GO即window
- 在运行函数中代码的时候,会首先查找VO中的变量(因此在第一次打印message的时候,不是window而是foo)
- 若此函数中 没有定义message,就会顺着作用域链,去查找上层作用域的该变量
-
情况三:有多层嵌套的函数关系 (一般不会写多层嵌套,容易造成回调地狱)
- 在代码执行过程前,首先会定义foo函数,此时不会有foo1的出现
- 在执行到foo()前,会创建相应的FEC、AO、scope chain(定义在全局作用域中,所有指向window),同时在AO中会声明foo1函数
- 执行完foo函数之后,FEC(foo)会弹出栈,就会运行GEC中的代码
- 在运行foo1()前,会创建相应的FEC 、AO、scope chain(定义在foo中,所以会先指向foo,而后指向window;注意!var变量,实际上会被V8引擎有所优化)
- 所以最终的结果就是,现在foo1的作用域中查找,没发现message,而后去foo中寻找,最后找到了window
来几道题练练手
ini
var n = 100;
function foo() {
n = 200;
}
foo();
console.log(n);
ini
var n = 100
function foo() {
console.log(n);
var n = 200
console.log(n);
}
foo()
ini
var n = 100;
function foo1() {
console.log(n);
}
function foo2() {
var n = 200;
console.log(n);
foo1();
}
foo2();
ini
var n = 100;
function foo2() {
console.log(n);
return
var n = 200
}
foo2();