【譯】在 JavaScript 中建立和填充任意長度的陣列

諍陽發表於2019-03-20

原文連結:2ality.com/2018/12/cre…

建立陣列的最佳方式是使用字面量:

const arr = [0,0,0];
複製程式碼

但也不總是最佳方案,例如在建立大型陣列時。這篇部落格就探討了在這些情況下該怎麼做。

1. 沒有空元素的陣列往往表現更好

在大多數程式語言中,陣列是連續的值序列。 在 JavaScript 中,Array 是一個將索引對映到元素的字典,它可以有空元素,而且空元素也有自己對應的索引,但它並不對映到元素上。例如下面這個陣列就有一個空元素在索引 1 的位置上。

> Object.keys(['a',, 'c'])
[ '0', '2' ]
複製程式碼

沒有空元素的陣列稱作密陣列,密陣列往往表現更好,因為它們可以連續(內部)儲存。一旦至少有一個空元素,內部表示必須改變。有兩種選擇:

  • 字典,查詢需要更多時間並且儲存開銷更大。
  • 連續的資料結構,具有空元素的標記值。檢查值是否是一個空元素,需要額外的時間。在任何一種情況下,如果引擎遇到一個空元素,它不能只返回 undefined ,而必須遍歷原型鏈並搜尋一個名稱為空元素索引的屬性,這需要花費更多時間。

在某些引擎中,例如 V8,切換到效能較低的資料結構是永久性的。即使所有空元素都新增上值,它們也不會切換回來。

有關 V8 如何表現陣列的更多資訊,請參閱 Mathias Bynens 的 V8 中的元素型別

2. 建立陣列

陣列建構函式

使用陣列建構函式來建立一個給定長度的陣列是一個很普遍的事情。

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

這種方法很方便,但它有兩個缺點:

  • 即使您稍後用完全添上值,這些空元素也會使此陣列略微變慢。
  • 空元素很少是元素的初始“值”。例如,0 更常見。

2.2 陣列建構函式加上 .fill() 方法

.fill() 方法改變現有陣列並使用指定值填充它。這有助於在通過新的 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())。

2.3 .push() 方法

const LEN = 3;shu
const arr = [];
for (let i=0; i < LEN; i++) {
  arr.push(0);
}
assert.deepEqual(arr, [0, 0, 0]);
複製程式碼

這一次,我們建立並填充了一個陣列而沒有在其中新增空元素。因此,在建立陣列後使用陣列應該比使用陣列建構函式更快。唉,建立陣列的速度較慢,因為引擎不得不隨著它的增長多次重新分配連續的內部表示。

2.4 填充未定義的陣列

Array.from()iterables型別 和 like-Array 型別 的值轉換為陣列。它將空元素視為未定義的元素。我們可以使用它將每個空元素轉換為未定義的:

> Array.from({length: 3})
[ undefined, undefined, undefined ]
複製程式碼

引數 {length:3} 是一個類似於陣列的物件,長度為3,只包含空元素。也可以使用 new Array(3) ,但通常會建立更大的物件。

陣列擴充套件僅適用於 iterable 的值,並且與 Array.from() 具有類似的效果:

> [...new Array(3)]
[ undefined, undefined, undefined ] 
複製程式碼

唉,Array.from() 通過 new Array() 建立它的結果,所以你仍然得到一個稀疏陣列。

2.5 使用 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(),它也將漏洞看作是未定義的元素:
> [... new Array(3).keys()]
[0,1,2]
複製程式碼

.keys() 返回一個可迭代的。我們使用擴充套件運算子將其轉換為陣列。

3. 補充:建立陣列

  • 填充空元素或未定義:
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}, () => ({}))
→ [{}, {}, {}] (unique objects)
複製程式碼
  • 整數範圍:
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]
複製程式碼

3.1 方法推薦

我更喜歡以下方法,重點是可讀性,而不是效能。

  • 你需要建立一個你將完全填充的空陣列,然後呢?
new Array(LEN)
複製程式碼
  • 你需要建立一個用原始值初始化的陣列嗎?
new Array(LEN).fill(0)
複製程式碼
  • 你需要建立一個用物件初始化的陣列嗎?
Array.from({length: LEN}, () => ({}))
複製程式碼
  • 你需要建立一系列整數嗎?
Array.from({length: END-START}, (x, i) => i+START)
複製程式碼

如果您正在處理整數或浮點數的陣列,請考慮 以此為目的建立的型別陣列。它們不能有空元素,並且總是用零初始化。

提示: 陣列效能通常沒有那麼重要

  • 在大多數情況下,我不會過分擔心效能問題。即使是空元素的陣列也非常快。我們可以更多得去考慮怎麼讓程式碼易於理解更有意義。

  • 此外,引擎優化的方式和位置也一直在發生變化。明天總是會比今天更快。

4. 致謝

感謝 Mathias Bynens 和 Benedikt Meurer 幫助我瞭解到 V8 細節。

5. 進一步閱讀

相關文章