面試官: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。數據結構

向頭部插入元素

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
相關文章
相關標籤/搜索