我们知道,V8是Chrome用来编译执行JavaScript的JavaScript引擎,所以如果我们要了解JavaScript,那么结合V8能让我们更好地理解,这篇博客将结合V8,来谈谈JavaScript中的对象

对象

什么是JavaScript的对象

什么是JavaScript的对象, JavaScript中的对象,并不是单单指Object,JavaScript这门语言本身,是基于对象的,可以说,JavaScript里面的大部分内容,都是由对象构成的。比如函数,数组,这些都是由对象构成的,甚至一些基本类型还有相关的内置对象诸如Number,String,Boolean

对象在运行时可以被动态修改属性的特点,让JavaScript变得十分灵活,但同时,也带来了一些难以理解的问题。

JavaScript对象可以说简单,也可以说复杂。简单来收,它不过是存储了一些属性,和这些属性对应的值而已,是一种key-value的结构。

而说它复杂,确实因为它的属性的值可以为任意类型。这也是它做为弱类型语言的特性,像Java,对象的每个属性对应的类型是确定的,而JavaScript并不。由于可以为任意类型,所以我们可以为一个属性,赋值为基本类型如Number、String、Boolean、null、undefiend等,也可以给它一个数组,一个普通对象,或者一个函数,使这个属性变成一个方法。这也可以看成,对象的属性的值可以有三种类型

  • 基本类型
  • 普通对象
  • 函数

此外,我们要明白的是,JavaScript虽然是基于对象的,但却不是面向对象的,学过Java的同学应该知道,面向对象三大特点,封装、继承、多态,而JavaScript里面并没有多态的实现。

除了多态没有实现外,继承上也与其他语言存在区别,JavaScript采用原型来实现继承,而原型在JavaScript中只是添加了一个属性,所以我们先来聊聊V8的对象属性存储策略。

V8的对象属性存储策略

我们所看到的对象属性存储,看起来像是字典的存储,哪个先存进去,遍历的时候就先存储哪个,但实际上,JavaScript的对象的属性存储并非完全如此,

一般情况下,属性确实是按存储进去的顺序,但是如果属性的名字是数值的话,那会按数值的形式存储

function Obj(){this[100] = 't100'this[50] = 't50'this.a = 'ta'this[20] = 't20'this.c = 'tc'this[25] = 't25'this[1] = 't1'this.b = 'tb'
}
let obj = new Obj()
for(let k in obj){console.log(k)
}


可以看到,打印结果里面,数字是按数字的顺序打印出来,而a、b、c是按我们写入的顺序打印出来的

直接在控制台查看obj对象也能看出顺序

之所以出现这样的结果,是因为在ECMAScript规范中定义了数字属性应该按照索引值大小升序排列,字符串属性根据创建时的顺序升序排列。

我们把按索引值大小排列的属性称为elements属性,而根据创建时的顺序排列的属性称为property属性。(这是按照Chrome浏览器控制台里Memory快照的属性名称取的)

因为elements属性是按索引顺序排序,所以删除和添加一个elements属性,都会引起一次属性排序,而property属性并不会。在V8中,为了能提升存储和访问这些属性的性能,对这两种属性采用了两种不同的线性结构来存储。

在遍历一个对象属性时,如上面我们打印的一样,V8会先去到elements属性中,根据索引遍历所有的elements属性,然后再去遍历property属性

使用两种线性结构来存储,解决了两种属性不同处理的问题,但在我们访问对象属性的时候,需要先判断这个属性的键是什么类型的,然后再根据类型,去到不同的结构中查找,增加了我们访问属性的时间

我们可以来看看Memory中的快照

我们可以看到,这里有elements属性,里面就是我们用索引排序的属性,但是,没有看到property属性,这和V8的权衡机制有关。

我们上面也提到了,使用两种线性结构,然后根据属性键值来遍历不同的属性,会降低访问效率,V8对此做了一定的权衡。当property属性不超过10个的时候,就直接将属性放在对象上,而不放到property属性上。

我们试试超过十个的property属性

function Obj(pNum,eNum) {for (let i = 0; i < pNum; i++) {let pKey = `property${i}`this[pKey] = `property${i}`}for (let i = 0; i < eNum; i++) {this[i] = `element${i}`}
}obj = new Obj(11,10)


可以看到,多了个properties属性,里面有个property10

我们将直接存储在对象上的property属性称为对象内属性,而在properties属性上的property属性有快属性慢属性两种。

对象内属性的访问是最快的,因为直接在对象上,而在properties属性上的属性,因为多了一次寻址,所以会比对象内属性慢,但是在properties属性上,其实还有细分。

快属性采用线性结构来存储,我们只需要通过索引就能访问到属性的值,但同时,带来的问题是如果我们插入和删除大量属性时,执行效率会变低。

所以当我们一个对象的属性多了的话,就会变成慢属性了。慢属性也是在properties上,但是它采用非线性的结构来存储属性值的。所以在插入删除时执行效率不会和线性结构一样,需要做大量的移动。

obj = new Obj(100,10)

看看创建了100个property属性的对象后内存快照

可以看到,内存中properties属性里面的属性已经是完全无序的,这表明这些属性采用了慢属性的存储方式

隐藏类

在上面的内存快照中,我们可以看到一个map属性,这个属性存放了描述命名属性,也就是隐藏类。隐藏类可以看成是一个描述对象结构的对象。

为什么要引入隐藏类,这是为了让JavaScript更快。我们知道,JavaScript是一门动态语言,在其运行过程中开发者能随时修改对象的属性和值,这种方式带来了灵活的同时,却让JavaScript对属性的访问变慢了。像Java这样的静态语言,因为类型一旦创建不可变,所以可以通过固定的偏移值对对象的属性进行访问,因此比动态语言快,而隐藏类借鉴了部分静态语言的特性。

V8采用的思路是,将JavaScript的对象静态化,即假设JavaScript的对象在创建之后就不会添加新的属性,也不会删除现有的属性。(实际上是会的)

V8为每一个JavaScript对象创建它自己的隐藏类,存放在map中,然后当我们去寻找一个对象的属性时,如果这个属性在map中有记录,那么我们去到map中,找到这个属性相对于对象的偏移值,在对象的地址上加上偏移值就可以找到这个属性了,这样比起上面的查找会更快。

而当我们创建了两个形状一样的对象时,即对象的属性个数一样且名称一样,那么这两个对象会共用一个隐藏类

function Person(name,age){this.name = namethis.age = age
}
var Bob = new Person('Bob',18)
var Mike = new Person('Mike',20)


可以看到上图中,两个对象的map隐藏类是一样的,即使我们不采用构造函数来创建对象,也是一样的

这么做,给我们访问对象又带来了一次提速,但是要记住,我们是有假设的,这样做的前提是对象不会增加属性,也不会删除属性,但实际上,这个假设是不成立的,那么也就是说,对于JavaScript来说,对象结构是会变的,那么隐藏类也就会发生改变。

我们可以修改上面的Mike对象,为其添加一个属性

Mike.work = 'coding'


可以从内存快照中看到,Mike的map隐藏类地址发生了改变,也就是说隐藏类发生了改变,符合了我们刚才说的结构改变隐藏类随之改变,这对于V8的执行效率来说,是一笔大的开销。

为了避免这种问题,我们可以在编写代码时注意一些优化细节

  1. 字面量声明对象时顺序一致
    两个对象的属性顺序不同也会创建不同的隐藏类,所以自己在使用字面量创建对象时,尽量顺序一致,减少创建隐藏类时间和隐藏类个数,像下面这两个对象
var p1 = { x:10,y:20 }
var p2 = { y:20,x:10 }


可以看到属性一样,却因为顺序不一样导致了隐藏类不同。

  1. 尽量使用使用字面量一次性写入所有的属性,一个个添加属性会导致多次创建隐藏类

  2. 减少使用delete,通过delete来删除对象属性,也会造成隐藏类的重新创建

虽然经过隐藏类的引入,我们访问对象属性的速度加快了,但还是有一些问题需要考虑,看看下面的代码

var Mike = {name:'Mike',age:18
}
function getAge(p){return p.age
}
for(let i = 0 ; i < 100 ; i++){getAge(Mike)
}

在上面的代码中,我们getAge去获取Mike的age属性的时候,通过map隐藏类,使用偏移值得到了对象的age属性值,但实际上,我们还是需要三步,去到对象的map属性,然后再找到偏移值,计算对象的地址和偏移值相加的值,才能找到age属性值,但实际上,我们一直在找一个相同的值,有什么方法可以优化这种寻找吗

内联缓存

上面的问题,V8采用了内联缓存(IC:Inline Cache)来处理。

知道怎么处理之前,我们先要知道IC的原理。简单来说,V8通过在JavaScript执行过程中观察一些调用点的关键数据,然后将这些关键数据存储起来,当再次调用这个函数时,就会去直接使用这些关键数据,节省了再次获取的时间。那么,V8是如何观察调用点的,又是如何存储关键数据,存储在哪的

首先,V8会为每个函数维护一个反馈向量(FeedBack Vector),记录函数在执行过程中的一些关键的中间数据,反馈向量实际上就是一个表,表中的每一项都是一个slot插槽,每个关键数据对应一个slot。每个插槽中包括了插槽的索引(slot index)、插槽的类型(type)、插槽的状态(state)、隐藏类(map)的地址、还有属性的偏移量

IC会存储三种插槽类型:存储类型、调用类型、加载类型,对应着数据的存储,函数的调用和对象属性的加载

看下面这段代码

function foo(){}
function loadX(o) { o.y = 4foo()return o.x
}
loadX({x:1,y:4})
  • 当我们执行loadX的时候,就会为其生成一个反馈向量
  • 执行到o.y时,就会创建一个存储类型的插槽,将操作结果放入插槽中,然后插入到反馈向量
  • 执行foo()时,首先要找到foo的地址,所以要创建一个加载类型的插槽,存储foo的地址,插入到反馈向量中,然后执行方法,创建一个调用类型的插槽,将调用结果放入插槽中,最后将这个插槽插入到反馈向量中
  • return o.x,获取到o.x的值,创建一个加载类型的插槽,存储o.x的值,插入到反馈向量中

之后我们再次调用这个函数时,只需要从反馈向量里面找到对应的插槽,就可以完成操作了

然而,这种性能提升的方式,局限于函数内部执行的内容是相同的,如果不相同,又会产生新的问题,看看下面这段代码

function loadX(o) { return o.x
}
var o = { x: 1,y:3}
var o1 = { x: 3, y:6,z:4}
for (var i = 0; i < 90000; i++) {loadX(o)loadX(o1)
}

我们可以看到,对象o和对象o1这两个对象的结构是不同的,也就是说,V8为它们创建的隐藏类也是不同的。

而在我们第一次调用loadX的时候,V8会将对象o的隐藏类和属性偏移值记录到反馈向量里面,但是当我们再次调用loadX的时候,V8会发现o1的结构和反馈向量里记录的隐藏类是不同的,所以这次无法使用反馈向量里面的缓存。

遇到这种情况时,V8会选择将同一个调用点的数据,存储在反馈向量里的同一个插槽里面,这样子,当这个函数再次被调用的时候,V8需要先去比对,插槽里记录的隐藏类,哪个和我当前传入的对象隐藏类相同,发现哪一个相同,就去使用哪一个记录,如果没有相同,则为这个插槽加入新的值。

现在我们知道了,一个反馈向量的一个插槽中可以包含多个隐藏类的信息,那么:

  • 如果一个插槽中只包含1个隐藏类,那么我们称这种状态为单态(monomorphic);
  • 如果一个插槽中包含了2~4个隐藏类,那我们称这种状态为多态(polymorphic);
  • 如果一个插槽中超过4个隐藏类,那我们称这种状态为超态(magamorphic)。

因此,我们需要尽可能地保持单态,避免多态和超态

原型链

聊完对象是怎么存储属性的,接下来我们可以聊聊JavaScript里面重要的继承机制,基于原型链的继承。

原型链是JavaScript重要的基础,也是面试里经常谈及的问题。这里我们谈谈V8里面是怎么去通过原型链实现继承的。

继承

首先,什么是继承,简单地说,我们通过让一个对象A继承另一个对象B,那么A可以直接调用B的方法,这就是继承。

继承的实现一般有两种,一种是基于类的继承,一种是基于原型的继承。

我们常看到的Java,C都是基于类的继承,这种继承方式的特点就是,我们能看到一些和继承相关的关键字,诸如public、private、protected、interface、class等,我们通过这些关键字,来实现类的声明和类的继承,同时限定类的访问。

而JavaScript使用的,是基于原型的继承,虽然JavaScript在ES6中出现了class,但它实际上只是一个语法糖而已,它的本质仍是基于原型的继承。JavaScript的原型继承,也只是通过引入了一个指向原型的属性而已。

如何实现原型继承

JavaScript的原型继承,通过引入了一个指向原型对象的隐藏属性__proto__,我们可以直接在console看到,也可以通过Memory快照看到对象的这个隐藏属性。

而JavaScript的原型继承,就是通过这个原型指向去寻找属性和方法,如果现在继承是C->B->A(C继承B,B继承A)。那么当我们去调用C的方法或者去访问C的属性,V8首先会在C这个对象上,寻找对应的属性方法,找不到的话,就沿着原型链的指向,去到对象B上找,如果还找不到,就继续沿着原型链,去A对象上找,如果还找不到,就看A是否原型链还有指向的对象,如果没有,返回undefined,如果是方法,此时执行就会报错。

综上,原型继承其实挺简单的,就是沿着原型链寻找而已,但是JavaScript实现继承的方式还有挺多的,在红宝书(《JavaScript高级程序设计》)里面,就说到了6种继承的方式,如果你看过红宝书,应该有印象,如果你没看过,建议你去看看,虽然书名高级,但在我看来只是本比较广泛的基础书籍而已,总的来说还是很有用的。

透过V8深入理解JavaScript的对象相关推荐

  1. 深入理解JavaScript中的属性和特性

    深入理解JavaScript中的属性和特性 JavaScript中属性和特性是完全不同的两个概念,这里我将根据自己所学,来深入理解JavaScript中的属性和特性. 主要内容如下: 理解JavaSc ...

  2. js 对象深拷贝_这一次,彻底理解JavaScript深拷贝

    导语 这一次,通过本文彻底理解JavaScript深拷贝! 阅读本文前可以先思考三个问题: JS世界里,数据是如何存储的? 深拷贝和浅拷贝的区别是什么? 如何写出一个真正合格的深拷贝? 本文会一步步解 ...

  3. 深入理解javascript原型和闭包(2)——函数和对象的关系

    上文(理解javascript原型和作用域系列(1)--一切都是对象)已经提到,函数就是对象的一种,因为通过instanceof函数可以判断. var fn = function () { }; co ...

  4. 深入理解JavaScript的闭包特性如何给循环中的对象添加事件

    初学者经常碰到的,即获取HTML元素集合,循环给元素添加事件.在事件响应函数中(event handler)获取对应的索引.但每次获取的都是最后一次循环的索引.原因是初学者并未理解JavaScript ...

  5. 深入理解JavaScript系列:根本没有“JSON对象”这回事!

    前言 写这篇文章的目的是经常看到开发人员说:把字符串转化为JSON对象,把JSON对象转化成字符串等类似的话题,所以把之前收藏的一篇老外的文章整理翻译了一下,供大家讨论,如有错误,请大家指出,多谢. ...

  6. 深入理解JavaScript类数组

    起因 写这篇博客的起因,是我在知乎上回答一个问题时,说自己在学前端时把<JavaScript高级程序设计>看了好几遍. 于是在评论区中,出现了如下的对话: 天啦噜,这话说的,宝宝感觉到的, ...

  7. JavaScript arguments对象

    1.在JavaScript中,arguments对象是比较特别的一个对象,实际上是当前函数的一个内置属性.arguments非常类似Array,但实际上又不是一个Array实例.可以通过如下代码得以证 ...

  8. 深入理解javascript函数系列第二篇——函数参数

    前面的话 javascript函数的参数与大多数其他语言的函数的参数有所不同.函数不介意传递进来多少个参数,也不在乎传进来的参数是什么数据类型,甚至可以不传参数.本文是深入理解javascript函数 ...

  9. JavaScript 复制对象与Object.assign方法无法实现深复制

    在JavaScript这门语言中,数据类型分为两大类:基本数据类型和复杂数据类型.基本数据类型包括Number.Boolean.String.Null.String.Symbol(ES6 新增),而复 ...

最新文章

  1. mysql数据没有真正提交,转MySQL 批量提交优化
  2. 10玩rust_有趣的 Rust 类型系统: Trait
  3. tomcat监控-psi-probe使用
  4. 【面试 多线程】【第九篇】多线程的问题
  5. 【青少年编程】【三级】打气球游戏
  6. Tomcat 的 Server 文件配置详解
  7. 41. 缺失的第一个正数 golang
  8. 数据结构-栈4-栈的应用-中缀转后缀
  9. 用python输入名字并打印_python的输出与输入
  10. 去哪儿-21-debuggiing-testing
  11. Unity中获取鼠标相对于UI组件的位置
  12. win7计算机管理快捷键,win7系统快捷键有哪些|win7常用的15个快捷键
  13. 生活中的 真、善、美
  14. WindowsCluster 由于在更新安全DNS区域时访问被拒绝,群集网络资源无法注册一个或多个关联的DNS名称
  15. 高效便捷组卷功能,学练考一体化让考试更轻松
  16. Linux 触摸屏 笔记本,Linux 5.2应该可以解决许多AMD Ryzen笔记本电脑触摸屏/触摸板无法工作的问题...
  17. 【理论恒叨】【立体匹配系列】经典SGM:(1)匹配代价计算之互信息(MI)
  18. 天地图 android studio,AndroidStudio 加载 天地图 (2019年后开发授权申请)
  19. UE4 error C7525: 内联变量至少需要 “/std:c++17“
  20. android平板提速,提升Android平板性能的十大技巧

热门文章

  1. linux找不到镜像文件,为什么我从硬盘安装Linux,系统总是提示找不到iso文件??...
  2. 运动模糊的图像修复调研
  3. 我的前半生之人物关系图
  4. Day3——页面旋转
  5. 对301、302的理解
  6. 读书笔记:大数据清洗技术 03
  7. 一文带你深入理解【Java基础】· 数组
  8. linux开机停在B7,[已解决] 开机时偶然地出现 “Timed out waiting for device /dev/sdb...” 并且导致启动失败...
  9. 云上创新,与时代前行(阿里云游记)
  10. 第十四周 【项目2-用文件保存的学生名单】若干名学生的学号 姓名和C++课、高数和英语成绩