js从⼀个数组⾥取四个值随机⽣成⼀个新数组_js的存储⽅式杂
记
前⾔
记录下V8中基本类型和对象的存储⽅式。
js的数据类型
js的数据⼤致上分为两种, ⼀种是原始类型(Boolean,Null,Undefined,Number,BigInt,String,Symbol), ⼀种是对象(Object)。
原始类型的数据放在栈中, 对象的数据放在堆中。
堆栈的区别
堆(heap)是不连续的内存区域,即数据可以任意存放, 主要存放的是对象等。
栈(stack)是⼀块连续的内存区域,每个区块按照⼀定次序存放(后进先出),栈中主要存放的是基本类型的变量的值以及指向堆中的数组或者对象的地址。
为什么要区分堆栈
变量主要是两种形式,⼀种内容短⼩(⽐如⼀个int整数),需要频繁访问,但是⽣命周期很短,通常只在⼀个⽅法内存活,⽽另⼀种内容可能很多(⽐如很长⼀个字符串),可能不需要太频繁的访问,
但⽣命周期较长,通常很多个⽅法中可能都要⽤到,那么⾃然将这两类变量分开就显得⽐较理性,⼀类存储在栈区,通常是局部变量、操作符栈、函数参数传递和返回值,另⼀类存储在堆区,通常是较⼤的结构体(或者OOP中的对象)、需要反复访问的全局变量。 堆区就是各种慢,申请内存慢,访问慢,修改慢,释放慢,整理慢(或者说GC垃圾回收),但优点也不⾔⽽喻,访问随机灵活,空间超⼤,在不超可⽤内存的情况下你要多⼤就给多⼤。 栈区就像临时⼯,⼲完就跑,所以超快,但是缺点也很多,⽐如⽣命周期短,⼀般只能在⼀个⽅法内存活,⼜⽐如你需要事先知道需要多⼤的栈(事实上绝⼤多数语⾔栈区要分配的⼤⼩编译期就确定了,Java就是这样),⽽且通常最⼤栈区可⽤内存都很⼩,你不可能往栈区⾥堆很多数据。
原始类型
原始类型有⼀个特点就是不可变。⽰例代码如下
// 例⼦1
var str = "abc";
str[0] = "d";
console.log(str) // abc
// 例⼦2
var str2 = "abc";
str2 = "dbc";
console.log(str) // dbc
例⼦1的数据没有改变, 例⼦2的数据却改变了, 实际上例⼦2是创建了⼀个新的字符串, 也就是内存开辟了⼀个新的区域给"dbc"使⽤。简单点来讲, 就是假设栈中存放了⼀个数据如"abc", 那么这个数据就永远不会改变, ⽽如果是如例⼦2中赋值了⼀个其他的字符串或者任何其他改变值的情况下, 栈中都会保留原来的"abc", 然后新开⼀个地⽅存放"dbc"。 类似下图:
为什么要把基础类型的值设成不可变
1. 为了安全
假设基础类型的值是可变的, 那么下⾯的代码会变得很奇怪
var strTest = "varaiable";
var fun = (str) => { str + "---ok" };
fun(strTest);
console.log(strTest) // varaiable---ok
// 可以看到strTest的值被改变了,特别是在map之类的对象中更为显著
var map = new Map()
var strTest = "t1";
map.set(strTest, 10);
strTest = "notT1";
<("t1"); // undefined;
<("notT1"); // 10
这样的代码容易造成更多的bug,特别是像java之类的多线程语⾔, 更有可能造成线程不安全的问题。
1. 为了共享
实际上, 基础类型中, 值⼀样的变量是共享⼀个内存区域的。
javascript全局数组
这样做的好处是避免额外的内存开销,提升效能。
当然, 这个前提是基础类型不可变, 不然如果str1的值变化了, str2的值也会跟着变化(实际上并没有对其操作)。
对象类型
V8中的对象(数组也是对象)存储相对来说⽐较复杂,他们是存放在堆⾥⾯的数据。并且格式⼤致如下:
这和很多资料说的是⽤Map实现不同, 很明显, 根据上图(来⾃v8的博客),起码可以说明不是使⽤Map来处理的。
V8是把对象中的属性分成两类, ⼀类是字符常量, ⼀类是数字or数字字符串(如"1"这种),并分别放在了两个数组,Properties和Elements。
普通的字符常量
先从普通的字符常量说起, 字符常量的存放⽅式⼜细分为三类。
第⼀类: In-object
实际上, 在⽣成⼀个对象的时候, v8会给该对象留下⼀些空间以分配属性(数量由对象的初始⼤⼩预先确定),这些属性直接存储在对象
本⾝上。这些是V8中最快的属性,因为⽆需任何间接访问即可访问它们,如下图:
第⼆类: Fast properties
v8的In-object空间并不多,通过对象字⾯量创建的⽆属性对象分配 4 个对象内属性存储(inobject_pro
perties)空间。当这些空间被使⽤完之后, 即会通过HideClass(隐藏类,有些也叫Map,这⾥统⼀叫隐藏类)来协助完成属性的快速访问。
HiddenClasses and DescriptorArrays
HiddenClass存储有关对象的元信息,包括该对象上的属性数量以及对该对象原型的引⽤。除此之外,HiddenClasses⾥⾯还有⼀个DescriptorArrays数组, 该数组存储了对象属性的信息。
即如下图:
这⾥⼀般会有⼀个疑惑, 为什么需要⼀个隐藏类, 我直接搞⼀个hashTable不是更快吗?
关于隐藏类及ICs的概率, 推荐阅读这⼀篇⽂章JavaScript 引擎基础:Shapes 和 Inline Caches, 概念清晰易懂,图⽂并茂。
这⾥简单说⼀下概念:
⾸先看下, 隐藏类是怎么来的
从图中可以看出, 隐藏类是通过⼀颗树来不断⽣成的,每添加⼀个属性都会新⽣成⼀个隐藏类节点(
添加数组索引属性不会创建新的),然后呢, 具有相同结构(相同属性,顺序相同)的对象具有相同的隐藏类。也就是说, 如果在上⾯的代码中加⼀个代码如下:
var a = {};
a.a = "ddd";
var b = {};
b.a = "3";
b.b = "test";
那么a的隐藏类是右边的第⼀个nofOwnDescriptors, b是第⼆个。对于程序代码来说, 实际上很多对象都是拥有相同的隐藏类。⽽隐藏类背后的主要动机是 Inline Caches 或 ICs 的概念。ICs 是促使 JavaScript 快速运⾏的关键因素!JavaScript 引擎利⽤ ICs 来记忆去哪⾥寻对象属性的信息,以减少昂贵的查次数。
⼤致就是每次将代码编译成字节码并读取属性时,都会根据隐藏类把该属性的位置保存起来,在下⼀次读取或者遇到拥有相同隐藏类的对象读取时,可以根据隐藏类提供的属性位置直接读取,⽽避免查过程。
第三类: Slow properties
最后⼀种⽅式即是字典存储⽅式。字典存储模式相对来说⽐较简单, 先看下官⽅提供的图:
简单点说, 就是隐藏类⾥⾯的DescriptorArrays会直接置为空, 然后把属性的值和元信息直接存储在properties数组中,并通过hash的⽅式进⾏get和set。
既然上⾯说了拥有隐藏类可以带来效能的提升, 为什么还要提供字典⽅式?
v8的原⽂如下:
However, if many properties get added and deleted from an object, it can generate a lot of time and memory overhead to maintain the descriptor array and HiddenClasses
⼤致意思是说,增加删除属性的操作过多会使⽤⼤量的时间和内存开销来维护descriptorArray 和 HiddenClasses。
最后, 什么时候是Fast properties(隐藏类), 什么时候是slow properties(字典模式)?
关于这⼀⽅⾯,推荐该系列⽂章奇技淫巧学 V8 之⼀,对象访问模式优化, 以下部分为引⽤ 新创建的⼩对象为Fast properties。执⾏如下操作的时候会变成slow properties
1. 动态添加过多的属性
2. 删除属性(delete)
3. 删除⾮最后添加的属性(V8 >= 6.0)
数组类型
数组的话种类⽐较多, 按官⽅的话说多达20种类型。
实际上, 数组⼀般是放到了⼀开始提的elements数组⾥⾯, 然后按索引读值, 这个⽐较简单, 说下其中⽐较典型的两种。
1. 存在缺失的元素,会按原型链串上去拿值,实际上就是对象原型链..
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论