怎樣在JavaScript中建立和填充任意長度的數組

翻譯:瘋狂的技術宅
原文: http://2ality.com/2018/12/cre...

本文首發微信公衆號:jingchengyideng
歡迎關注,天天都給你推送新鮮的前端技術文章html


建立數組的最佳方法是經過字面方式:前端

const arr = [0,0,0];

不過這並非長久之計,好比當咱們須要建立大型數組時。這篇博文探討了在這種狀況下應該怎麼作。程序員

沒有空洞的數組每每表現得更好

在大多數編程語言中,數組是連續的值序列。在 JavaScript 中,Array 是一個將索引映射到元素的字典。它能夠存在空洞(holes) —— 零和數組長度之間的索引沒有映射到元素(「缺失索引」)。例如,下面的 Array 在索引 1 處有一個空洞:es6

> Object.keys(['a',, 'c'])
[ '0', '2' ]

沒有空洞的數組也稱爲 dense packed。密集數組每每表現更好,由於它們能夠連續存儲(內部)。一旦出現了空洞,內部表示就必須改變。咱們有兩種選擇:面試

  • 字典。查找時會消耗更多時間,並且存儲開銷更大。
  • 連續的數據結構,對空洞進行標記。而後檢查對應的值是不是一個空洞,這也須要額外的時間。

不論是哪一種狀況,若是引擎遇到一個空洞,它不能只返回 undefined,它必須遍歷原型鏈並搜索一個名稱爲「空洞索引」的屬性,這須要花費更多時間。編程

在某些引擎中,例如V8,若是切換到性能較低的數據結構,這種改變將會是永久性的。即便全部空洞都被填補,它們也不會再切換回來了。segmentfault

關於 V8 是如何表示數組的,請參閱Mathias Bynens的文章「V8中的元素類型」。數組

建立數組

Array 構造函數

若是要建立具備給定長度的 Array,經常使用的方法是使用 Array 構造函數 :安全

const LEN = 3;
const arr = new Array(LEN);
assert.equal(arr.length, LEN);
// arr only has holes in it
assert.deepEqual(Object.keys(arr), []);

這種方法很方便,可是有兩個缺點:微信

  • 即使你稍後再用值把數組徹底填滿,這種空洞也會使這個 Array 略微變慢。
  • 空洞的默認值通常不會是元素的初始「值」。常見的默認值是零。

Array 構造函數後面加上 .fill() 方法

.fill()方法會更改當前的 Array 並使用指定的值去填充它。這有助於在用 new Array() 建立數組後對其進行初始化:

const LEN = 3;
const arr = new Array(LEN).fill(0);
assert.deepEqual(arr, [0, 0, 0]);

警告:若是你用對象做爲參數去 .fill() 一個數組,全部元素都會引用同一個實例(也就是這個對象沒有被克隆多份):

const LEN = 3;
const obj = {};

const arr = new Array(LEN).fill(obj);
assert.deepEqual(arr, [{}, {}, {}]);

obj.prop = true;
assert.deepEqual(arr,
  [ {prop:true}, {prop:true}, {prop:true} ]);

稍後咱們會遇到的一種填充方法( Array.from() )則沒有這個問題。

.push() 方法

const LEN = 3;
const arr = [];
for (let i=0; i < LEN; i++) {
  arr.push(0);
}
assert.deepEqual(arr, [0, 0, 0]);

這一次,咱們建立並填充了一個數組,同時裏面沒有出現漏洞。因此操做這個數組時應該比用構造函數建立的更快。不過 建立 數組的速度比較慢,由於引擎可能須要隨着數組的增加屢次從新分配連續的內存。

使用 undefined 填充數組

Array.from() 將 iterables 和相似數組的值轉換爲 Arrays ,它將空洞視爲 undefined 元素。這樣能夠用它將每一個空洞都轉換爲 undefined

> Array.from({length: 3})
[ undefined, undefined, undefined ]

參數 {length:3} 是一個長度爲 3 的相似 Array 的對象,其中只包含空洞。也可使用 new Array(3),但這樣通常會建立更大的對象。
下面這種方式僅適用於可迭代的值,而且與 Array.from()具備相似的效果:

> [...new Array(3)]
[ undefined, undefined, undefined ]

不過 Array.from()經過 new Array() 建立它的結果,因此你獲得的仍然是一個稀疏數組。

使用 Array.from() 進行映射

若是提供映射函數做爲其第二個參數,則可使用 Array.from() 進行映射。

用值填充數組

  • 使用小整數建立數組:

    > Array.from({length: 3}, () => 0)
    [ 0, 0, 0 ]
  • 使用惟一(非共享的)對象建立數組:

    > Array.from({length: 3}, () => ({}))
    [ {}, {}, {} ]

按照數值範圍進行建立

  • 用升序整數數列建立數組:

    > Array.from({length: 3}, (x, i) => i)
    [ 0, 1, 2 ]
  • 用任意範圍的整數進行建立:

    > const START=2, END=5;
    > Array.from({length: END-START}, (x, i) => i+START)
    [ 2, 3, 4 ]

另外一種建立升序整數數組的方法是用 .keys(),它也將空洞看做是 undefined 元素:

> [...new Array(3).keys()]
[ 0, 1, 2 ]

.keys()返回一個可迭代的序列。咱們將其展開並轉換爲數組。

備忘速查:建立數組

用空洞或 undefined填充:

  • new Array(3)
    [ , , ,]
  • Array.from({length: 2})
    [undefined, undefined]
  • [...new Array(2)]
    [undefined, undefined]

填充任意值:

  • const a=[]; for (let i=0; i<3; i++) a.push(0);
    [0, 0, 0]
  • new Array(3).fill(0)
    [0, 0, 0]
  • Array.from({length: 3}, () => ({}))
    [{}, {}, {}] (惟一對象)

用整數範圍填充:

  • Array.from({length: 3}, (x, i) => i)
    [0, 1, 2]
  • const START=2, END=5; Array.from({length: END-START}, (x, i) => i+START)
    [2, 3, 4]
  • [...new Array(3).keys()]
    [0, 1, 2]

推薦的模式

我更喜歡下面的方法。個人側重點是可讀性,而不是性能。

  • 你是否須要建立一個空的數組,之後將會徹底填充?

    new Array(LEN)
  • 你須要建立一個用原始值初始化的數組嗎?

    new Array(LEN).fill(0)
  • 你須要建立一個用對象初始化的數組嗎?

    Array.from({length: LEN}, () => ({}))
  • 你須要建立一系列整數嗎?

    Array.from({length: END-START}, (x, i) => i+START)

若是你正在處理整數或浮點數的數組,請考慮Typed Arrays —— 它就是爲這個目的而設計的。它們不能存在空洞,而且老是用零進行初始化。

提示:通常來講數組的性能可有可無

  • 對於大多數狀況,我不會過度擔憂性能。即便是帶空洞的數組也很快。使代碼易於理解更有意義。
  • 另外引擎優化的方式和位置也會發生變化。今天最快的方案可能明天就不是了。

致謝

  • 感謝 Mathias Bynens 和 Benedikt Meurer 幫我瞭解 V8 的詳細信息。

擴展閱讀


歡迎繼續閱讀本專欄其它高贊文章:


本文首發微信公衆號:jingchengyideng

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章

相關文章
相關標籤/搜索