首页>>前端>>JavaScript->深入JavaScript运行原理

深入JavaScript运行原理

时间:2023-12-01 本站 点击:0

浏览器的工作原理

大家有没有深入思考过: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,并在Windows 7或更高版本,macOS 10.12+和使用x64,IA-32,ARM或MIPS处理器的Linux系统上运行。

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

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

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

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

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

运行代码

js引擎内部有一个执行上下文栈(Execution Context Stack,简称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引擎就实现了Lazy Parsing(延迟解析)的方案,它的作用是将不必要的函数进行预解析,也就是只解析暂时需要的内容,而对函数的全量解析是在函数被调用时才会进行;

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

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

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

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

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

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

JavaScript的执行过程

1. 初始化全局对象

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

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

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

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

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

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

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

全局的代码块为了执行会构建一个 Global Execution Context(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'。

函数调用函数执行过程

打印结果:Hello Global

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

变量环境和记录

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

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

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

作用域提升面试题

第一题

    var n = 100;    function foo(){      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

var n = 100;function foo(){    var n = 200}foo()console.log(n)

第二题

function foo(){    console.log(n)    var n= 200    console.log(n)}var n = 100foo()

输出结果:undefined; 200

分析:

第三题

        var n = 100        function foo1(){            console.log(n)           //2、100        }        function foo2(){            var n =200            console.log(n)           //1、200            foo1()        }        foo2()        console.log(n)           //3、100

输出结果:见注释

第四题

        var a= 100        function foo(){            console.log(a)            return            var a=100        }        foo()

输出结果:undefined

分析:

第五题

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

输出结果:undefined ; 10

原文:https://juejin.cn/post/7099834026541711391


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:/JavaScript/6424.html