建立陣列的最佳方式是使用字面量:
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. 進一步閱讀
- “探索ES6”中的“型別陣列”一章
- “探索ES6”中的“ES6和陣列中的空元素”