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

深入JavaScript运行原理

深入V8引擎原理

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

Parse模块

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

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();
​
相关推荐
大家的林语冰18 分钟前
前端周刊:axios 疑遭朝鲜黑客“钓鱼“;CSS 新函数上线;npm 上线深色主题;Oxlint 兼容表;ESLint 支持 Temporal......
前端·javascript·css
哀木1 小时前
一个简单的套壳方案,就能让你的 Agent 少做重复初始化
前端
问心无愧05132 小时前
ctf show web入门27
前端
小村儿2 小时前
给 AI Agent 装上"长期记忆":Karpathy 的 LLM Wiki 思想,我做成了工具
前端·后端·ai编程
竹林8182 小时前
用ethers.js连接MetaMask实现Web3钱包登录:从踩坑到稳定运行的完整记录
前端·javascript
heyCHEEMS2 小时前
如何用 Recast 实现静态配置文件源码级读写
前端·node.js
心连欣2 小时前
从零开始,学习所有指令!
前端·javascript·vue.js
review445432 小时前
大模型和function calling分别是如何工作的
前端
东东同学2 小时前
耗时一个月,我把 Nuxt 首屏性能排障经验做成了一个 AI Skill
前端·agent
冴羽3 小时前
超越 Vibe Coding —— AI 辅助编程指南
前端·ai编程·vibecoding