V8引擎之JavaScript如何执行(一)

简介: V8引擎之JavaScript如何执行(一)

什么是v8?


V8 是 JavaScript 虚拟机的一种。将人类能够理解的编程语言 JavaScript,翻译成机器能够理解的机器语言


在 V8 出现之前,所有的 JavaScript 虚拟机所采用的都是解释执行的方式,这是 JavaScript 执行速度过慢的一个主要原因。(执行过程中要先编译源码,再执行,多了个编译阶段 大部分解释性语言是动态语言,动态语言的数据类型是在运行时检查的,这又耗费了额外的时间)。


JavaScript 借鉴了很多语言的特性,比如 C 语言的基本语法、Java 的类型系统和内存管理、Scheme 的函数作为一等公民,还有 Self 基于原型(prototype)的继承机制。

V8 中使用的隐藏类(Hide Class),这是将 JavaScript 中动态类型转换为静态类型的一种技术,可以消除动态类型的语言执行速度过慢的问题,如果你熟悉 V8 的工作机制,在你编写 JavaScript 时,就能充分利用好隐藏类这种强大的优化特性,写出更加高效的代码。


JavaScript 是一种自动垃圾回收的语言,V8 在执行垃圾回收时,会占用主线程的资源,如果我们编写的程序频繁触发垃圾回收,那么无疑会阻塞主线程。


v8执行js代码


V8 又是怎么执行 JavaScript 代码的呢?


其主要核心流程分为编译和执行两步。首先需要将 JavaScript 代码转换为低级中间代码或者机器能够理解的机器代码,然后再执行转换后的代码并输出执行结果。


计算机执行高级语言的方式


  • 解释执行,需要先将输入的源代码通过解析器编译成中间代码,之后直接使用解释器解释执行中间代码,然后直接输出结果


网络异常,图片无法展示
|


  • 编译执行。采用这种方式时,也需要先将源代码转换为中间代码,然后我们的编译器再将中间代码编译成机器代码。通常编译成的机器代码是以二进制文件形式存储的,需要执行这段程序的时候直接执行二进制文件就可以了。还可以使用虚拟机将编译后的机器代码保存在内存中,然后直接执行内存中的二进制代码。


网络异常,图片无法展示
|


V8 率先引入了即时编译(JIT)的双轮驱动的设计,这是一种权衡策略,混合编译执行和解释执行这两种手段,给 JavaScript 的执行速度带来了极大的提升。


网络异常,图片无法展示
|


通过一些列转换后生成字节码(即中间代码),然后解释器将直接执行输出结果。在解释执行字节码的过程中,如果发现了某一段代码会被重复多次执行,那么监控机器人就会将这段代码标记为热点代码。 V8 就会将这段字节码丢给优化编译器,优化编译器会在后台将字节码编译为二进制代码,然后再对编译后的二进制代码执行优化操作,优化后的二进制机器代码的执行效率会得到大幅提升。


V8 执行一段 JavaScript 代码所经历的主要流程了,这包括了:


  • 初始化基础环境。


  • 解析源码生成 AST 和作用域。


  • 依据 AST 和作用域生成字节码。


  • 解释执行字节码。(解释执行)


  • 监听热点代码。


  • 优化热点代码为二进制的机器代码。(编译执行)


  • 反优化生成的二进制机器代码。


v8是如何调用函数的呢


其实在 V8 内部,会为函数对象添加了两个隐藏属性,具体属性如下图所示


网络异常,图片无法展示
|


如果某个函数没有设置函数名,该函数对象的默认的 name 属性值就是 anonymous。

如果一个函数赋值给一个变量,那么该函数的函数名将不能使用。


const bar = function foo() {
    }
    // console.log(foo.name) // 报错foo is not defined
    console.log(bar.name) // foo
    foo() // foo is not defined


code 属性,其值表示函数代码,以字符串的形式存储在内存中。当执行到一个函数调用语句时,V8 便会从函数对象中取出 code 属性值,也就是函数代码,然后再解释执行这段函数代码。


V8是怎样提升对象属性访问速度的?


JavaScript 对象像一个字典,字符串作为键名,任意对象可以作为键值,可以通过键名读写键值。


然而在 V8 实现对象存储时,并没有完全采用字典的存储方式,这主要是出于性能的考量。因为字典是非线性的数据结构,查询效率会低于线性的数据结构,V8 为了提升存储和查找效率,采用了一套复杂的存储策略。


常规属性和排序属性


在 ECMAScript 规范中定义了数字属性应该按照索引值大小升序排列字符串属性根据创建时的顺序升序排列。 前者被称为排序属性,elements。后者被称为常规属性,properties。


对象包含了两个隐藏属性:elements 属性和 properties 属性,elements 属性指向了 elements 对象,在 elements 对象中,会按照顺序存放排序属性,properties 属性则指向了 properties 对象,在 properties 对象中,会按照创建时的顺序保存了常规属性。

分解成这两种线性数据结构之后,如果执行索引操作,那么 V8 会先从 elements 属性中按照顺序读取所有的元素,然后再在 properties 属性中读取所有的元素,这样就完成一次索引操作。


但是如果我们读取一个属性值时,他就需要查找对应的elements或者properties对象,然后又在elements或者properties对象中查找对应的属性。这无疑增加了一步查询操作。所以 V8 采取了一个权衡的策略以加快查找属性的效率,这个策略是将部分常规属性直接存储到对象本身,我们把这称为对象内属性 (in-object properties)。


网络异常,图片无法展示
|


但是对象内属性的数量是固定的,默认是10个,如果查出对象分配的空间,那么他们依旧保存在常规属性中。


如果一个对象的属性过多时,V8 就会采取另外一种存储策略,那就是慢属性策略,但慢属性的对象内部会有独立的非线性数据结构(词典) 作为属性存储容器。所有的属性元信息不再是线性存储的,而是直接保存在属性字典中。


网络异常,图片无法展示
|


总之,就是如果在对象中有两个隐藏属性(elements, properties)。


  • 如果向对象中添加数字属性,那么它将通过数字大小进行排序放进elements中存储。


  • 如果向对象中添加非数字属性,


  • 个数小于10个,那么它将直接存储在对象本身上。


  • 如果大于10个,并且不是很多的话,那么它将多余10个的属性放在properties属性中。并且按照创建属性的顺序存入。


  • 如果添加特别多的属性或者存在反复添加或者删除属性的操作,那么它将properties存储的数据结构,改为非线性进行存储。这样虽然降低了查找速度,但是却提升了修改对象的属性的速度。 可以通过这个例子查看,运行代码,通过浏览器的memory查看。


function Foo(property_num,element_num) {
        //添加可索引属性
        for (let i = 0; i < element_num; i++) {
            this[i] = `element${i}`
        }
        //添加常规属性
        for (let i = 0; i < property_num; i++) {
            let ppt = `property${i}`
            this[ppt] = ppt
        }
    }
    // 分别传入索引属性和非属性属性个创建个数。
    var bar = new Foo(10,10)


网络异常,图片无法展示
|


var bar = new Foo(20,10);


网络异常,图片无法展示
|


var bar = new Foo(100,10);


网络异常,图片无法展示
|


立即执行函数


因为小括号之间存放的必须是表达式,所以如果在小阔号里面定义一个函数,那么 V8 就会把这个函数看成是函数表达式,执行时它会返回一个函数对象。


因为函数立即表达式也是一个表达式,所以 V8 在编译阶段,并不会为该表达式创建函数对象。这样的一个好处就是不会污染环境,函数和函数内部的变量都不会被其他部分的代码访问到。


表达式是不会在编译阶段执行的。函数声明的本质是语句,而函数表达式的本质则是表达式。


V8是如何实现对象继承的


对象的原型:每个对象都有一个隐藏的__proto__的属性。


原型对象:__proto__指向的对象。并且指向构造函数的prototype。


继承就是一个对象可以访问另外一个对象中的属性和方法,在JavaScript 中,我们通过原型和原型链的方式来实现了继承特性。


通过new创建对象时。V8 会在背后悄悄地做了以下几件事情。


var dog = {}  
    dog.__proto__ = Constructor.prototype
    Constructor.call(dog)
    return dog


v8是如何查找变量的


作用域链就是将一个个作用域串起来,实现变量查找的路径。


作用域是在编译阶段就确定的。


全局作用域是在 V8 启动过程中就创建了,且一直保存在内存中不会被销毁的,直至 V8 退出。 而函数作用域是在执行该函数时创建的,当函数执行结束之后,函数作用域就随之被销毁掉了。


因为 JavaScript 是基于词法作用域(静态作用域) 的,词法作用域就是指,查找作用域的顺序是按照函数定义时的位置来决定的


var name = '极客时间'
    var type = 'global'
    function foo(){
        var name = 'foo'
        console.log(name) // foo
        console.log(type) // global
    }
    function bar(){
        var name = 'bar'
        var type = 'function'
        foo()
    }
    bar()


V8是怎么实现1+“2”的


对机器语言来说,所有的数据都是一堆二进制代码,CPU 处理这些数据的时候,并没有类型的概念,CPU 所做的仅仅是移动数据,比如对其进行移位,相加或相乘。


每种语言都定义了自己的类型,还定义了如何操作这些类型,另外还定义了这些类型应该如何相互作用,我们就把这称为类型系统


v8会严格根据ecmaScript规范来执行操作。


在两个类型的变量进行相加时,V8 会提供了一个 ToPrimitive 方法,其作用是将 a 和 b 转换为原生数据类型,其转换流程如下:


  • 先检测该对象中是否存在 valueOf 方法,如果有并返回了原始类型,那么就使用该值进行强制类型转换。


  • 如果 valueOf 没有返回原始类型,那么就使用 toString 方法的返回值。


  • 如果 vauleOf 和 toString 两个方法都不返回基本类型值,便会触发一个 TypeError 的错误


网络异常,图片无法展示
|


两个原生类型相加,如果其中一个值的类型是字符串时,则另一个值也需要强制转换为字符串,然后做字符串的连接运算。在其他情况时,所有的值都会转换为数字类型值,然后做数字的相加。


js代码运行时环境


在执行 JavaScript 代码之前,V8 就已经准备好了代码的运行时环境,这个环境包括了堆空间和栈空间、全局执行上下文、全局作用域、内置的内建函数、宿主环境提供的扩展函数和对象,还有消息循环系统。


浏览器为 V8 提供基础的消息循环系统、全局变量、Web API,而 V8 的核心是实现了 ECMAScript 标准。V8 还提供了垃圾回收器、协程等基础内容,不过这些功能依然需要浏览器(nodejs)环境的配合才能完整执行。


网络异常,图片无法展示
|


堆空间和栈空间


在 Chrome 中,只要打开一个渲染进程,渲染进程便会初始化 V8,同时初始化堆空间和栈空间。



栈空间主要是用来管理 JavaScript 函数调用的,栈是内存中连续的一块空间,同时栈结构是“先进后出”的策略。在函数调用过程中,涉及到上下文相关的内容都会存放在栈上,比如原生类型、引用到的对象的地址、函数的执行状态、this 值等都会存在在栈上。当一个函数执行结束,那么该函数的执行上下文便会被销毁掉。


栈空间的最大的特点是空间连续,所以在栈中每个元素的地址都是固定的,因此栈空间的查找效率非常高,但是通常在内存中,很难分配到一块很大的连续空间,因此,V8 对栈空间的大小做了限制,如果函数调用层过深,那么 V8 就有可能抛出栈溢出的错误。

如果有一些占用内存比较大的数据,或者不需要存储在连续空间中的数据,使用栈空间就显得不是太合适了,所以 V8 又使用了堆空间。



堆空间是一种树形的存储结构用来存储对象类型的离散的数据


和栈空间不同,存放在堆空间中的数据是不要求连续存放的,从堆上分配内存块没有固定模式的,你可以在任何时候分配和释放它


全局执行上下文和全局作用域


V8 初始化了基础的存储空间之后,接下来就需要初始化全局执行上下文和全局作用域了,这两个内容是 V8 执行后续流程的基础。


当 V8 开始执行一段可执行代码时,会生成一个执行上下文。V8 用执行上下文来维护执行当前代码所需要的变量声明、this 指向等


执行上下文中主要包含三部分,变量环境、词法环境和 this 关键字。而词法环境中,则包含了使用 let、const 等变量的内容。


网络异常,图片无法展示
|


全局执行上下文在 V8 的生存周期内是不会被销毁的,它会一直保存在堆中


全局作用域和全局执行上下文的关系,其实你可以把作用域看成是一个抽象的概念。全局上下文中包含全局作用域和块级作用域,函数作用域。


构造事件循环系统


有了堆栈空间,生成了全局上下文和全局作用域后,V8 还需要有一个主线程,用来执行 JavaScript 和执行垃圾回收等工作。


只有一个主线程依然不行,因为如果你开启一个线程,在该线程执行一段代码,那么当该线程执行完这段代码之后,就会自动退出了,执行过程中的一些栈上的数据也随之被销毁,下次再执行另外一个段代码时,你还需要重新启动一个线程,重新初始化栈数据,这会严重影响到程序执行时的性能。


这就需要利用浏览器的事件循环了。

网络异常,图片无法展示
|


渲染进程专门有一个 IO 线程用来接收其他进程传进来的消息


例如XMLHttpRequest


网络异常,图片无法展示
|


JavaScript 是如何支持块级作用域


他将var声明的变量放在当前执行上下文的变量环境中。将let, const声明的变量放在词法环境中。


function foo(){
        var a = 1
        let b = 2
        {
          let b = 3
          var c = 4
          let d = 5
          console.log(a)
          console.log(b)
        }
        console.log(b) 
        console.log(c)
        console.log(d)
    }   
    foo()


当进入函数的作用域块时,作用域块中通过 let 声明的变量,会被存放在词法环境的一个单独的区域中,这个区域中的变量并不影响作用域块外面的变量。


词法环境内部, 维护了一个小型栈结构。栈底是函数最外层的变量,进入一个作用域块后,就会把该作用域块内部的变量压到栈顶;当作用域执行完成之后,该作用域的信息就会从栈顶弹出,这就是词法环境的结构。


当查找变量时,先查找词法环境中的,如果没找到,再去变量环境中查找。


我们需要好好理解这句话:在块作用域内,let声明的变量被提升,但变量只是创建被提升,初始化并没有被提升,在初始化之前使用变量,就会形成一个暂时性死区。


网络异常,图片无法展示
|


一些思考


  • 为什么不建议使用delete删除属性。结合快属性和慢属性。 快属性: 保存在线性数据结构中的属性。 慢属性: 保存在非线性数据结构中的属性。


如果删除属性在线性结构中,删除后需要移动元素,开销较大,而且可能需要将慢属性重排到快属性。 如果删除属性在properties对象中,查找开销较大。


  • function fn(){setTimeOut(fn,0)}这种调用会不会导致内存溢出? 不会,因为这是异步调用,下次执行fn函数时,已经在新的栈中执行了,所以当前栈不会发生溢出!


  • 作用域和执行上下文是什么关系?


  1. 作用域是静态的,函数定义的时候就已经确定了。


  1. 执行上下文是动态的,调用函数时候创建,结束后还会释放。


该文章总结自极客时间李兵老师的图解 Google V8课程。

time.geekbang.org/column/arti…


相关文章
|
22天前
|
JavaScript 前端开发 NoSQL
【MongoDB 专栏】MongoDB 的 JavaScript 引擎与脚本执行
【5月更文挑战第11天】MongoDB 的 JavaScript 引擎允许在服务器端直接执行脚本,提升效率并实现定制化操作。脚本环境提供独立但与数据库关联的运行空间,引擎负责脚本的解析、编译和执行。执行过程包括脚本提交、解析、编译和执行四个步骤。掌握脚本逻辑设计和 JavaScript 语言特性对于高效利用这一功能至关重要。例如,通过脚本可以计算商品总销售额,增强数据库操作的灵活性。
【MongoDB 专栏】MongoDB 的 JavaScript 引擎与脚本执行
|
9月前
|
存储 JavaScript 前端开发
从 V8 优化看高效 JavaScript
从 V8 优化看高效 JavaScript
54 0
|
22天前
|
Web App开发 前端开发 JavaScript
探索 V8 引擎的内部:深入理解 JavaScript 执行的本质
探索 V8 引擎的内部:深入理解 JavaScript 执行的本质
探索 V8 引擎的内部:深入理解 JavaScript 执行的本质
|
22天前
|
JavaScript 前端开发 开发者
Vue.js深度解析:前端开发的生产力引擎
Vue.js深度解析:前端开发的生产力引擎
49 0
|
7月前
|
自然语言处理 JavaScript 前端开发
V8 是如何执行 JavaScript 代码的
V8 是如何执行 JavaScript 代码的
70 0
|
8月前
|
Web App开发 自然语言处理 JavaScript
带你读《现代Javascript高级教程》十、JavaScript引擎的工作原理:代码解析与执行(1)
带你读《现代Javascript高级教程》十、JavaScript引擎的工作原理:代码解析与执行(1)
|
8月前
|
Web App开发 缓存 JavaScript
带你读《现代Javascript高级教程》十、JavaScript引擎的工作原理:代码解析与执行(2)
带你读《现代Javascript高级教程》十、JavaScript引擎的工作原理:代码解析与执行(2)
|
8月前
|
JavaScript 前端开发 算法
带你读《现代Javascript高级教程》十一、JavaScript引擎的垃圾回收机制(1)
带你读《现代Javascript高级教程》十一、JavaScript引擎的垃圾回收机制(1)
|
8月前
|
JavaScript 前端开发 算法
带你读《现代Javascript高级教程》十一、JavaScript引擎的垃圾回收机制(2)
带你读《现代Javascript高级教程》十一、JavaScript引擎的垃圾回收机制(2)
|
10月前
|
JavaScript 前端开发
JS引擎的执行机制event loop
JS引擎的执行机制event loop
47 0