JavaScript中的数组

120 阅读4分钟

JavaScript的中的数组(自己学习)

JavaScript的数组和C++、Java等语言中的数组不太一样。JavaScript中的数组可以存放不同类型的数据、数组长度可变。V8引擎中对JS的数组进行了封装,使其有两种实现方式,分别是快数组和慢数组。两种数组有各自的优缺点,使用情况以及相互转换。

  • 快数组:底层是连续的内存空间,通过索引直接定位。
  • 慢数组:底层是哈希表,通过计算哈希值进行定位。

1、数组的创建

//1.通过构造函数创建数组
const arr1 = new Array(10) //创建长度为10的数组
//2.通过字面量的形式创建
const arr2 = [1, 2, "Apple", "Banana"]

2、V8引擎对数组的实现

V8中JSArray继承自JSObject,所以,数组是一个特殊的对象,就可以很好的解释了为什么JS中的数组可以存放不同类型的数据,因为其是一个对象,内部也是key-value的存储形式。key为0,1,2,3索引,value为数组的元素。数组不能使用任意字符串作为元素的索引,必须使用非负整数(或他们的字符串形式)。通过非整数设置或者访问不会设置或从数组列表本身检查元素,但是会设置或访问与该数组的对象属性集合相关的变量。数组的对象属性和数组元素列表是分开的,数组的遍历和修改操作不能应用于这些命名属性。

const arr = [2, 3, 4]
console.log(arr[1]) //3
console.log(arr[01]) //3
console.log(arr["1"]) //3
conso.log(arr["01"]) //undefined
arr["01"] = 1
arr.forEach(item => {
    console.log(item)
})
//2,3,4
//不会打印出1,因为"01"是数组的对象属性和数组的列表是分开的的

3、快数组

快数组是一种线性的存储方式。新创建的空数组默认的存储方式为快数组,快数组的长度是可变的,可以根据元素的增加和删除动态调整存储空间的大小,内部是通过扩容和收缩的机制实现。

  • 扩容:扩容后的新容量=旧容量的1.5倍 + 16,扩容后将数组拷贝到新的空间中
  • 收缩: 收缩的判断机制capacity >= 2 * length + 16,收缩的大小需要根据length + 1 和old_length进行判断,是将空出的空间全部收缩掉还是只收缩二分之一。

4、慢数组

慢数组是一种字典的内存形式,不用开辟大块连续的内存空间,节省了内存,但是由于需要维护HashTable,其效率会比快数组低。

5、快慢数组之间的转换

  • 快->慢:①新容量 >= 3 * 扩容后的容量 * 2 会转换为慢数组 ②index - capacity >=JSObject::KMaxGap时,变成字典模式,其中kMaxGap是常量1024,也就是新加入的HOLEY(空洞)大于1024,则转换为慢数组。
  • 处于哈希表实现的数组,在每次空间变化时,都会检查其空间占用量,若其空洞元素减小到一定程度,则会将其转化为快数组的模式。当慢数组的元素可存放在快数组中且长度在smi之间且仅节省了50%的空间,则转变为快数组

6、V8中的数组类型

V8对不同的数组类型都有优化,其中比较典型的类型有

  • PACKED_SMI_ELEMENTS
  • PACKED_DOUBLE_ELEMENTS
  • PACKED_ELEMENTS
  • HOLEY_SMI_ELEMENTS
  • HOLEY_DOUBLE_ELEMENTS
  • HOLEY_ELEMENTS

PACKED是指连续数组;HOLEY是指稀疏数组;SIM是指数据类型为32位整型;DOUBLE是指浮点类型;而什么类型都不写是指数组的类型杂糅着字符串、函数等混合类型

在这些类型中,最高效的类型为PACKED_SMI_ELEMENTS类型,一个简单的空数组的默认类型为PACKED_SMI_ELEMENTS

const foo = [] //PACKED_SMI_ELEMENTS
foo.push(1) //PACKED_SMI_ELEMENTS
foo.push(2) //PACKED_SMI_ELEMENTS
foo.push(3) //PACKED_SMI_ELEMENTS

数组类型也可以随着向数组中添加或删除元素而动态改变。

const foo = []; // element kind: PACKED_SMI_ELEMENTS
foo.push(1); // element kind: PACKED_SMI_ELEMENTS
foo[100] = 2; // element kind: HOLEY_SMI_ELEMENTS
foo.push(5.2); // element kind: PACKED_DOUBLE_ELEMENTS
foo.pop(); // element kind: PACKED_DOUBLE_ELEMENTS
foo.push('bar'); // element kind: HOLEY_ELEMENTS

值得注意的是一旦数组的类型发生降级,例如上面的例子,即时我们删除了元素5.2数组的类型也不会变成SMI,即降级是不可逆的,PACKED只能变成更糟的HOLEY,SMI只能往更糟的DOUBLE和空类型变化,且两种变化都不可逆。

element-kind-transition-v8_tysqsn.webp

7、性能优化技巧

  • 永远不要访问索引越界的数组元素

  • const foo = [1,2,3]
    console.log(foo[100])
    //在这种情况下,V8引擎需要花费高昂的代价在原型链上查找,影响性能。
    
  • 避免给数组初始化长度

  • const foo = new Array(3) //element kind:HOLEY_SMI_ELEMENTS
    foo[0] = 1
    foo[1] = 2
    foo[2] = 3
    //这将创建一个初始容量为3的HOLEY_SMI_ELEMENTS数组。但是,我们只存储了PACKED_SMI_ELEMENTS元素。为什么我们要失去PACKED_SMI_ELEMENTS的优化呢