面试官:JavaScript的数组有什么特殊之处?

数组是前端开发者最经常使用的数据结构了,咱们在项目中无时不刻在操做着数组,例如将列表组件的数据储存在数组里、将须要渲染成条形图的数据一样储存在一个数组里,虽然咱们常用数组,可是不少人并不了解JavaScript数组的本质。javascript

本节咱们将从JavaScript数组的使用、内存模型两大部分进行讲解,但愿经过这个小节,让你们对JavaScript的数组有更深的认识。前端

在正是开始这节以前,请你们思考一个问题,JavaScript的数组有什么特殊之处?java

数组的使用

数组是咱们最经常使用的数据结构,不少基于数组的操做你们也足够熟悉了,咱们不会在这里罗列数组的API,由于MDN数组这一部分足够权威也足够全面,咱们会简单介绍下重点的数组方法,为接下来的内容作铺垫。node

数组的建立与初始化

若是你以前学过其它语言相似于c++/java等,你可能会用一下方法建立并初始化一个数组:c++

const appleMac = new Array('Mac Book Air', 'iMac', 'Mac Book Pro', 'Mac pro')
复制代码

固然这在JavaScript中是能够的,但并不主流方法,一般人们建立并初始化数组用的是字面量的方式:es6

const appleMac = ['Mac Book Air', 'iMac', 'Mac Book Pro', 'Mac pro']
复制代码

在es6中引入了两个新方法,一样能够建立数组:编程

  • Array.of() 返回由全部参数组成的数组,不考虑参数的数量或类型,若是没有参数就返回一个空数组
  • Array.from()从一个类数组或可迭代对象中建立一个新的数组

这两个方法分别解决了两个问题,Array.of()解决了构造函数方法建立数组时单个数字引发了怪异行为。api

const a = new Array(3);   // (3) [empty × 3] 构造函数方法单个数组会被用于数组长度
const b = Array.of(3);    // [3]
复制代码

Array.from()解决了『类数组』的转化问题,以前咱们将类数组转化为数组的方法广泛用的是Array.prototype.slice.call(arguments)这种偏Hack的方法,Array.from()的出现将其规范化,在之后的转化中咱们最好按照标准的Array.from()方法进行转化。数组

数组的操做

数组的操做有数十种之多,咱们不可能一一讲到,具体使用也能够看MDN,咱们只讲两个对本节比较重要的api。markdown

向头部插入元素

unshift操做是最多见的向数组头部添加元素的操做

const arr = [1, 2, 3]

arr.unshift(0) // arr = [0, 1, 2, 3,]

复制代码

向尾部插入元素

push操做是最多见的向数组尾部添加元素的操做

const arr = [1, 2, 3]

arr.push(4) // arr = [1, 2, 3, 4]

复制代码

内存模型

编程语言的内存一般要经历三个阶段

  1. 分配内存
  2. 对内存进行读、写
  3. 释放内存(垃圾回收)

数组的建立对应着第一阶段,数组的操做对应着第二阶段。

所以,如今有一个问题,咱们分别用push和unshift往数组的尾部和头部添加元素,谁的速度更快?

连续内存

若是你比较了解相关数据结构内存的话应该会知道,数组是会被分配一段连续的内存,如图:

2019-06-18-10-21-29

那么当咱们向这个数组最后push元素6的时候,只须要将后面的一块内存分配给6便可。

而unshift则不一样,由于是向数组头部添加元素,数组为了保证连续性,头部以后的元素须要依次向后移动。

unshift的本质相似于下面的代码:

for (var i=numbers.length; i>=0; i--){
      numbers[i] = numbers[i-1];
    }
    numbers[0] = -1;
复制代码

2019-06-18-10-30-59

因为unshift出发了全部元素内存后移,致使性能远比push要差。

我在node10.x版本下做了一个实验:

function unshiftFn() {
    const a = []

    console.time('unshift')
    for (var i=0;i<100000;i++) {
        a.unshift(1);
    }

    console.timeEnd('unshift')
}

function pushFn() {
    const a = []

    console.time('push')
    for (var i=0;i<100000;i++) {
        a.push(1);
    }

    console.timeEnd('push')
}

unshiftFn() // unshift: 2297.383ms
pushFn() // push: 3.760ms

复制代码

咱们看见二者的速度差了很是多,并且若是你不断调整for循环的次数,会发现当次数越多的时候,unshift操做就越慢,由于须要日后移的元素也就越多。

而形成这个差别的正是由于数组是被储存为一块连续内存致使的,这就形成了数组的『插入』『删除』的性能都不好,由于咱们一旦删除或者插入元素,其余元素为了保持一块连续的内存都不得不产生大量元素位移,这是性能的杀手。

非连续内存

咱们开头就有一个问题:JavaScript的数组有什么特殊之处?

固然咱们会说不少JavaScript的特殊之处,什么支持字面量声明建立,支持储存不一样类型数据、动态性等等。

而本质上JavaScript数组的特殊之处在于JavaScript的数组不必定是连续内存。

而维基百科关于数组的定义:

在计算机科学中,数组数据结构(英语:array data structure),简称数组(英语:Array),是由相同类型的元素(element)的集合所组成的数据结构,分配一块连续的内存来存储。

若是是这样的话,JavaScript的数组彷佛并非严格意义上的数组,那么为何上一小节说数组是分配了连续内存呢?这不是自相矛盾了吗?

JavaScript的数组是否分配连续内存取决于数组成员的类型,若是统一是单一类型的数组那么会分配连续内存,若是数组内包括了各类各样的不一样类型,那么则是非连续内存。

非连续内存的数组用的是相似哈希映射的方式存在,好比声明了一个数组,他被分配给了100一、20十一、108八、1077四个非连续的内存地址,经过指针链接起来造成一个线性结构,那么当咱们查询某元素的时候实际上是须要遍历这个线性链表结构的,这十分消耗性能。

2019-06-18-11-08-47

而线性储存的数组只须要遵循这个寻址公式,进行数学上的计算就能够找到对应元素的内存地址。

a[k]_address = base_address + k * type_size

咱们作一个简单的实验,咱们不断向数组插入元素,但对比的双方是非线性储存的数组和线性储存的同构数组:

const total = 1000000

function unshiftContinuity() {
    const arr = new Array(total)
    arr.push({name: 'xiaomuzhu'});
    console.time('unshiftContinuity')
    for(let i=0;i<total; i++){
        arr[i]=i
    }
    console.timeEnd('unshiftContinuity')
}

function unshiftUncontinuity() {
    const arr = new Array(total)
    console.time('unshiftUncontinuity')
    for (let i=0;i<total;i++) {
        arr[i]=i
    }

    console.timeEnd('unshiftUncontinuity')
}

unshiftContinuity() // unshiftContinuity: 71.050ms
unshiftUncontinuity() // unshiftUncontinuity: 1.691ms

复制代码

咱们看到,非线性储存的数组其速度比线性储存的数组要慢得多。


因为做者并无阅读过JavaScript引擎的源码,因此这并非一手资料,若是有错误很是欢迎指出来,我会及时更正。


参考:

How are JavaScript arrays represented in physical memory?


2019-07-12-15-10-54
相关文章
相关标签/搜索