问答文章1 问答文章501 问答文章1001 问答文章1501 问答文章2001 问答文章2501 问答文章3001 问答文章3501 问答文章4001 问答文章4501 问答文章5001 问答文章5501 问答文章6001 问答文章6501 问答文章7001 问答文章7501 问答文章8001 问答文章8501 问答文章9001 问答文章9501

深入JavaScript运行原理

发布网友 发布时间:2024-09-17 06:31

我来回答

1个回答

热心网友 时间:2024-10-04 06:39

浏览器的工作原理

大家有没有深入思考过:JavaScript代码,在浏览器中是如何被执行的?

浏览器的渲染过程

在这个执行过程中,HTML解析的时候遇到了JavaScript标签,应该怎么办呢?

会停止解析HTML,而去加载和执行JavaScript代码;

那么,JavaScript代码由谁来执行呢?

JavaScript引擎

为什么需要JavaScript引擎呢?

高级的编程语言都是需要转成最终的机器指令来执行的;事实上我们编写的JavaScript无论你交给浏览器或者Node执行,最后都是需要被CPU执行的;但是CPU只认识自己的指令集,实际上是机器语言,才能被CPU所执行;所以我们需要JavaScript引擎帮助我们将JavaScript代码翻译成CPU指令来执行;V8引擎V8引擎的原理

我们来看一下官方对V8引擎的定义:

V8是用C++编写的Google开源高性能JavaScript和WebAssembly引擎,它用于Chrome和Node.js等。

它实现ECMAScript和WebAssembly,并在Windows7或更高版本,macOS10.12+和使用x64,IA-32,ARM或MIPS处理器的Linux系统上运行。

V8可以独立运行,也可以嵌入到任何C++应用程序中

代码被解析,v8引擎内部会帮助我们创建一个对象(GlobalObject->go)

该对象所有的作用域(scope)都可以访问;

里面会包含Date、Array、String、Number、setTimeout、setInterval等等;

其中还有一个window属性指向自己;

运行代码

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

V8引擎的架构

V8引擎本身的源码非常复杂,大概有超过100w行C++代码,通过了解它的架构,我们可以知道它是如何对JavaScript执行的:

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

如果函数没有被调用,那么是不会被转换成AST的;

Parse的V8官方文档:https://v8.dev/blog/scanner

Ignition是一个解释器,会将AST转换成ByteCode(字节码)

同时会收集TurboFan优化所需要的信息(比如函数参数的类型信息,有了类型才能进行真实的运算);

如果函数只调用一次,Ignition会执行解释执行ByteCode;

Ignition的V8官方文档:https://v8.dev/blog/ignition-interpreter

TurboFan是一个编译器,可以将字节码编译为CPU可以直接执行的机器码;

如果一个函数被多次调用,那么就会被标记为热点函数,那么就会经过TurboFan转换成优化的机器码,提高代码的执行性能;

但是,机器码实际上也会被还原为ByteCode,这是因为如果后续执行函数的过程中,类型发生了变化(比如sum函数原来执行的是number类型,后来执行变成了string类型),之前优化的机器码并不能正确的处理运算,就会逆向的转换成字节码;

TurboFan的V8官方文档:https://v8.dev/blog/turbofan-jit

V8引擎的解析图(官方)V8执行的细节

Parser就是直接将tokens转成AST树架构;

PreParser称之为预解析,为什么需要预解析呢?

这是因为并不是所有的JavaScript代码,在一开始时就会被执行。那么对所有的JavaScript代码进行解析,必然会影响网页的运行效率;

所以V8引擎就实现了LazyParsing(延迟解析)的方案,它的作用是将不必要的函数进行预解析,也就是只解析暂时需要的内容,而对函数的全量解析是在函数被调用时才会进行;

比如我们在一个函数outer内部定义了另外一个函数inner,那么inner函数就会进行预解析;

那么我们的JavaScript源码是如何被解析(Parse过程)的呢?

Blink将源码交给V8引擎,Stream获取到源码并且进行编码转换;

Scanner会进行词法分析(lexicalanalysis),词法分析会将代码转换成tokens;

接下来tokens会被转换成AST树,经过Parser和PreParser:

生成AST树后,会被Ignition转成字节码(bytecode),之后的过程就是代码的执行过程(后续会详细分析)。

JavaScript的执行过程1.初始化全局对象

js引擎会在执行代码之前,会在堆内存中创建一个全局对象:GlobalObject(GO)

该对象所有的作用域(scope)都可以访问;

里面会包含Date、Array、String、Number、setTimeout、setInterval等等;

其中还有一个window属性指向自己;

2.执行上下文栈(调用栈)

js引擎内部有一个执行上下文栈(ExecutionContextStack,简称ECS),它是用于执行代码的调用栈。

那么现在它要执行谁呢?执行的是全局的代码块:

全局的代码块为了执行会构建一个GlobalExecutionContext(GEC);

GEC会被放入到ECS中执行;

GEC被放入到ECS中里面包含两部分内容:

这个过程也称之为变量的作用域提升(hoisting)

第一部分:在代码执行前,在parser转成AST的过程中,会将全局定义的变量、函数等加入到GlobalObject中,但是并不会赋值;

第二部分:在代码执行中,对变量赋值,或者执行其他的函数;

3.GEC被放入到ECS中4.GEC开始执行代码作用域提升

第二行打印num1会显示undefined

全局代码执行过程函数

这里打印name,在foo函数内部找不到name,根据变量的真实查找路径是沿着作用域链来查找的规则,就会在父级作用域里找name,这里foo的父级作用域就是全局,所以打印出来就是why

函数嵌套

在foo函数中再嵌套一个bar函数,在bar函数中打印这个name会怎么样呢?

在代码执行前(预编译),在parser转成AST的过程中,会将全局定义的变量、函数等加入到GlobalObject中,但是并不会赋值,bar函数这里保存的是内存地址0xb00,指向它所对应的内存空间

执行代码的时候,给AD里面的num:undefined等赋值,变成num:123等

执行第13行代码的时候,这里是调用了bar函数,然后它就会在函数调用栈中创建一个函数执行上下文

打印name的时候,先在AO里面找是否有name,没有-->去上层作用域找,没有-->沿着作用域链继续往上层找,找到了name='why',所以最终打印出来是'why'。

函数调用函数执行过程

打印结果:HelloGlobal

执行过程:首先,预编译{message:undefined,foo:0xa00,bar:0xb00};然后执行代码给各项赋值{message:"HelloGlobal",foo:0xa00,bar:0xb00};按照顺序,先执行bar(),那么就是先调用了bar(),然后这里就有个函数执行上下文,这里面的VO对象是AO,在这里面var出来的message是储存在AO里面,接下来再调用foo(),又有一个函数执行上下文,这里执行的代码是打印message,那么首先找的是foo()里面有没有message,没找到就沿着作用域链向上查找,找到了父级中有个message,所以打印出来为"HelloGlobal"

变量环境和记录

其实我们上面的讲解都是基于早期ECMA的版本规范:

在最新的ECMA的版本规范中,对于一些词汇进行了修改:

通过上面的变化我们可以知道,在最新的ECMA标准中,我们前面的变量对象VO已经有另外一个称呼了变量环境VE。

作用域提升面试题

第一题

varn=100;functionfoo(){n=200}foo()console.log(n)

输出结果:200

分析:首先预编译->{n:undefined,foo:0xa00},然后编译时对n赋值,调用foo(),这里foo函数被调用就创建了一个函数执行上下文,这里执行代码n=200,然后n在foo函数对象里面找不到,就会到上层去找,找到父级里面的n,并赋值为200,所以就相当于修改了go里面的值,所以最后打印的时候,n为200。

注意:这里要区分一下,如果是这种情况,输出的n就是100

varn=100;functionfoo(){varn=200}foo()console.log(n)

第二题

functionfoo(){console.log(n)varn=200console.log(n)}varn=100foo()

输出结果:undefined;200

分析:

第三题

varn=100functionfoo1(){console.log(n)//2、100}functionfoo2(){varn=200console.log(n)//1、200foo1()}foo2()console.log(n)//3、100

输出结果:见注释

第四题

vara=100functionfoo(){console.log(a)returnvara=100}foo()

输出结果:undefined

分析:

第五题

functionfoo(){vara=b=100//这里相对于vara=10//b=10//b这里没有定义,js会默认把他放在全局}foo()console.log(a)console.log(b)

输出结果:undefined;10

原文:https://juejin.cn/post/7099834026541711391
声明声明:本网页内容为用户发布,旨在传播知识,不代表本网认同其观点,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。E-MAIL:11247931@qq.com
富士打印机怎么设置ip地址? 超市购物模拟器收银员 微信好友辅助安全登录验证该怎么做 培育钻石和天然钻石本质上有区别吗 培育钻石有哪些优缺点? 买的基金怎么取出来吗? 如何把鸡毛去掉 他先对女孩有好感,女孩对他表白了,但是他一直这样对待女孩?_百度... 主保护包括哪些 经常带孩子散步有哪些好处和坏处? 电饭煲加热方式有哪些 电饭煲什么方式加热最好 电饭煲买IH加热的好吗? javascript中if条件判断的相关细节 电饭煲什么加热方式好一点 电饭煲加热方式哪种好聚能循环加热 深度剖析JavaScript事件循环机制原理 如何有效排出耳朵中的水? 如何将手机号码拉黑? 一机双卡怎样拉黑号码 ie浏览器如何保存密码 百度浏览器保存密码的操作步骤是怎样的? 如何保存浏览器密码? 怎样保存网页的密码? 小红书线索私信投放API对接开发说明 goodjob, 这是什 夫妻同区迁户口需要什么手续? goodjob,这是什么意思? 港股实时行情API|港股实时行情数据对接 线下api对接是什么意思? 胃烧心吃啥药 烧心吃什么药最好 js基本语法遵循的标准(js基本语法的思维导图) jQuery入门-细节讲解2 红娘子可以养殖吗哪里可以买到幼虫? 章鱼会喷出墨汁吗 用微波炉加热锡纸盒可以吗? 小年微信说说大全 小年微信说说大全搞笑 微信过小年祝福 适合祝福小年的微信 2022年度微信小年祝福语 最新小年微信祝福语 适合小年的祝福语 留学 离职感言 夏天海参煲什么汤好 WinXP系统重装后文件夹拒绝访问怎么办 WindowsXP系统下怎样更改Vista的文件夹权限【图文教程】 六年级第二单元作文多彩的活动怎么写 长沙 跑得快 3人打牌 请高手指点 长沙玩跑得快技 三星手机以前那么火,现在人们为什么很少用 芝士怎样加热才能吃 贝壳名下都有哪些中介 honorhryal00a怎么刷机