重学JavaScript高级(三):深入JavaScript运行原理

913 阅读6分钟

深入JavaScript运行原理

深入V8引擎原理

  • 使用C++编写的,可以独立运行,也可以嵌入到任何C++应用程序中

image.png

Parse模块

  • Parse模块会将JS代码转成AST(抽象语法树),这是因为解释器并不直接认识JS代码

Ignition模块

  • 是一个解释器,会将AST转成ByteCode
  • 同时会收集TurboFan优化需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算)
  • 如果函数只调用一次,Ignition会直接解释执行ByteCode
  • 官方文档v8.dev/blog/igniti…

TurboFan模块

  • 是一个编译器,可以将字节码编译为CPU可以直接执行的机器码
  • 如果一个函数被多次调用,那么这个函数就会被标记成热点函数,就会经过 TurboFan转换成优化的机器码,提高代码的执行性能
  • 但是机器码实际上也会被还原为ByteCode,这是因为如果后续函数执行过程中,类型发生了变化(比如两数相加的函数,传入了两个字符串) ,之前优化的机器码并不能正确的处理运算,就会你想的转成字节码
  • 官方文档

JS执行过程

  • 以下代码如何运行(先以ES6之前的语法为例子)
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属性指向自己
  • image.png

执行上下文(补充概念)

js引擎内部有一个执行上下文栈(Execution Context Stack 简称ECS),用于执行代码的调用栈

  • 代码为了执行,会在ECS中创建执行上下文(Execution Context,简称EC),之后放到ECS中;而全局代码创建的是GEC,全局上下文

image.png

  • 多个活跃的执行上下文,会在逻辑上形成栈的数据结构

    • 对于这句话的理解,即在执行全局函数的时候,遇到了需要执行其他函数的代码,比如fn(),此时在ECS(上下文执行栈)中,生成一个新的EC(执行上下文),并压入栈顶

全局代码执行流程(过程二)

  • 全局代码执行前,会创建一个全局执行上下文GEC,那么GEC放入到ECS中包含两部分

    • 在代码执行前,会在V8引擎中parse模块转成AST的过程中,会将全局定义的变量(会声明undefined),函数(函数会同时被创建)放入到VO中,而全局的VO就是堆内存中的GO

      • VO对象(只有在正式执行代码前才会创建),每一个执行上下文GEC都会关联一个VO(Variable Object),变量和函数声明都会被添加到这个对象中
      • 全局的的GO会作为VO,会创建作用域链,VO,this
      • 通过这个过程就可以说明,在给一个变量赋值之前,去访问它,是undefined,而访问一个函数却可以被执行
      • 同时在这个过程中,有一个变量提升的过程

    image.png

    • 在代码执行中,对变量进行赋值,或者执行其他函数

image.png

函数代码执行过程(过程三)

在全局代码的执行过程中,遇到了函数的调用,那么,接下来会怎么执行呢?

  • 通过上述的学习,我们知道,在执行 一段新的代码时候,都会在 上下文执行栈(ECS) 中创建 执行上下文(EC) ,那么遇到函数也不例外,就会创建一个FEC
  • 同时在正式执行代码前,还会再堆内存中,创建一个 VO对象,用于存放函数的变量声明等内容,针对于函数的VO,是用AO来代替的(若只是声明函数,没有调用,就不会有AO的创建)
  • AO中,还会声明一个arguments,用于存放传入的参数,是有值的

image.png

  • 接下来就会正式运行代码,将变量赋值

image.png

  • 函数执行完毕之后,上下文执行栈中的 函数执行上下文就会弹出栈,而其对应的AO,是否会销毁,就需要看后续的代码,大概率是会进行销毁的

思考,多次调用foo,以及foo中调用其他的函数,会如何执行

作用域和作用域链(变量的查找要在定义的位置开始,和在哪里调用的无关)

  • 目前,最常见的就是 全局作用域和函数作用域注意:声明对象用的大括号,不会生成作用域

  • 当在一个作用域中使用一个变量,首先会在自己的作用域中查找是否存在这个变量,若没有的话,就会在上层作用域查找,这样就会形成一条 作用域链

  • 作用域和作用域链是在函数定义的时候,就确定好的(我们可以借助浏览器的调试工具去查看)

  • 接下来我们可以看三种情况

    • 情况一,全局代码(具体流程见上面的内容)

      • 在执行log函数的时候,会现在自己的作用域中去查找变量,没有找到就会顺着作用域链去查找
      • 作用域链创建和VO在代码执行前就创建好的

image.png

  • 情况二:全局代码中,遇见简单的函数

    • 会先对函数进行声明,创建对应的函数对象
    • 在正式执行代码前,会创建相应的VO对象和作用域链
    • 该函数的作用域链指向GO即window
    • 在运行函数中代码的时候,会首先查找VO中的变量(因此在第一次打印message的时候,不是window而是foo)
    • 若此函数中 没有定义message,就会顺着作用域链,去查找上层作用域的该变量

image.png

  • 情况三:有多层嵌套的函数关系 (一般不会写多层嵌套,容易造成回调地狱)

    • 在代码执行过程前,首先会定义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

image.png

来几道题练练手

var n = 100;
​
function foo() {
  n = 200;
}
foo();
​
console.log(n);
var n = 100
function foo() {
  console.log(n);
​
  var n = 200
​
  console.log(n);
}
​
foo()
var n = 100;
​
function foo1() {
  console.log(n);
}
​
function foo2() {
  var n = 200;
  console.log(n);
  foo1();
}
​
foo2();
var n = 100;
​
function foo2() {
  console.log(n);
  return
  var n = 200
}
​
foo2();