从Chrome源码看JS Array的实现

我们在上一篇介绍了JS Object的实现,这一篇将进一步介绍JS Array的实现。

在此之前,笔者将Chromium升级到了最新版本60,上一次是在元旦的时候下的57,而当前最新发布的稳定版本是57。57是三月上旬发布的,所以Chrome发布一个大版本至少用了两、三个月的时间。Chrome 60的devTool增加了很多有趣的功能,这里顺便提一下:

从Chrome源码看JS Array的实现_第1张图片

例如把没有用到的CSS/JS按比例标红,增加了全页的截屏功能,和一个本地代码的编辑器:

从Chrome源码看JS Array的实现_第2张图片

回到正文。

JS的Array是一个万能的数据结构,为什么这么说呢?因为首先它可以当作一个普通的数组来使用,即通过下标找到数组的元素:

然后它可以当作一个栈来使用,我们知道栈的特点是先进后出,栈的基本操作是出栈和入栈:

同时它还可以当作一个队列,队列的特点是先进先出,基本操作是出队和入队:

甚至它还可以当作一个哈表表来使用:

另外,它还可以随时随地增删数组中任意位置的元素:

JS Array一方面提供了很大的便利,只要用一个数据结构就可以做很多事情,使用者不需要关心各者的区别,使得JS很容易入门。另一方面它屏蔽了数据结构的概念,不少写前端的都不知道什么是栈、队列、哈希、树,特别是那些不是学计算机,中途转过来的。然而这往往是不可取的。

另外一点是,即使是一些前端的老司机,他们也很难说清楚,这些数组函数操作的效率怎么样,例如说随意地往数组中间增加一个元素不会有性能问题么。所以就很有必要从源码的角度看一下数组是怎么实现的。

1. JS Array的实现

先看源码注释:

这里说明一下,如果不熟悉C/C++的,那把它成伪码就好了。

源码里面说了,JSArray有两种模式,一种是快速的,一种是慢速的,快速的用的是索引直接定位,慢速的使用用哈希查找,这个在上一篇《从Chrome源码看JS Object的实现》就已经提及,由于JSArray是继承于JSObject,所以它也是同样的处理方式,如下面的:

增加一个2000的索引时,array就会被转成慢元素。

如下的数组:

把a打印出来:

– map = 0x939ebe04359 [FastProperties]

– prototype = 0x27e86e126289

– elements = 0xe70c791d4e9 [FAST_SMI_ELEMENTS (COW)]

– length = 3

– properties = 0x2b609d202241 {

    #length: 0x2019c3e58da9 (const accessor descriptor)

}

elements= 0xe70c791d4e9 {

           0: 8

           1: 1

           2: 2

}

它有一个length的属性,它的elements有3个元素,按索引排列。当给它加一个2000的索引时:

 

打印出来的array变成:

– map = 0x333c83f9dbb9 [FastProperties]

– prototype = 0xdcc53ba6289

– elements = 0x21a208a1d541 [DICTIONARY_ELEMENTS]

– length = 2001

– properties = 0x885d1402241 {

    #length: 0x1f564a958da9 (const accessor descriptor)

}

– elements= 0x21a208a1d541 {

   2: 2 (data, dict_index: 0, attrs: [WEC])

   0: 8 (data, dict_index: 0, attrs: [WEC])

   2000: 10 (data, dict_index: 0, attrs: [WEC])

   1: 1 (data, dict_index: 0, attrs: [WEC])

}

elements变成了一个慢元素哈希表,哈希表的容量为29。

由于快元素和慢元素上一节已经有详细讨论,这一节将不再重复。我们重点讨论数组的操作函数的实现。

2. Push和扩容

数组初始化大小为4:

 

执行push的时候会在数组的末尾添加新的元素,而一旦空间不足时,将进行扩容。

在源码里面push是用汇编实现的,在C++里面嵌入的汇编。这个应该是考虑到push是一个最为常用的操作,所以用汇编实现提高执行速度。在汇编的上面封装了一层,用C++调的封装的汇编的函数,在编译组装的时候,将把这些C++代码转成汇编代码。

计算新容量的函数:

如上代码新容量等于 :

new_capacity = old_capacity /2 + old_capacity + 16

即老的容量的1.5倍加上16。初始化为4个,当push第5个的时候,容量将会变成:

new_capacity = 4 / 2 + 4 + 16 = 22

接着申请一块这么大的内存,把老的数据拷过去:

由于复制是用的memcopy,把整一段内存空间拷贝过去,所以这个操作还是比较快的。

再把新元素放到当前length的位置,再把length增加1:

可以来改点代码玩玩,我们知道push执行后的返回结果是新数组的长度,尝试把它改成返回老数组的长度:

重新编译Chrome,在控制台上执行比较如下:

从Chrome源码看JS Array的实现_第3张图片

右边的新Chrome返回了4,左边正常的Chrome返回5.

3. Pop和减容

push是用汇编实现,而pop的逻辑是用C++写的。在执行pop的时候,第一步,获取到当前的length,用这个length – 1得到要删除的元素,然后调用setLength调整容量,最后返回删除的元素: