稀疏陣列真心話大冒險

陳澤輝發表於2019-02-18

An adventure in sparse arrays
JavaScript 稀疏陣列與孔(hole) – 簡書

概念

稀疏陣列(sparse array)

稀疏陣列與密集陣列最大的不同,就是稀疏陣列中可以有“孔”(hole)。
絕大多數 JavaScript 中的稀疏陣列預設是都帶孔的。(ES標準並沒有這樣規定)

孔(hole)

孔是邏輯上存在於陣列中,但物理上不存在與記憶體中的那些陣列項。在那些僅有少部分項被使用的陣列中,孔可以大大減少記憶體空間的浪費。
孔更像是 undefined ,但並非真正的沒有被定義,僅僅是陣列中的孔沒有被賦值。

簡單實現(大冒險)

new Array(1) // hole × 1
[ , ] // hole × 1
[ 1, ] // int(1), no holes, length: 1
[ 1, , ] // int(1) and hole × 1
複製程式碼

上例中可以看出,陣列逗號分隔符後邊的元素完全被忽略了。

再看看容易混淆的情況:

const a = [ undefined, , ];
console.log(a[0]) // undefined
console.log(a[1]) // undefined
複製程式碼

以上陣列的值都為 undefined ,但是又並非都為 undefined。可以看出,a[0] 的值是 undefineda[1] 是一個孔。

我們用 prop in object 驗證一下

const a = [ undefined, , ];
console.log(0 in a) // true
console.log(1 in a) // false
複製程式碼

這樣看起來是不是很明朗了?但是真正的孔為 undefined 的真相是什麼,我們繼續。
使用原型鏈 prototype 進行一次賦值,來看看情況:

// elsewhere in boobooland
Array.prototype[1] = `fool!`
// and in my code
const a = [ undefined, , ];
console.log(0 in a) // true
console.log(1 in a) // true … ?
複製程式碼

那問題來了,如果原型鏈被汙染了,這種方式驗證孔的存在就顯得不嚴謹了。當然使用 proper 的方式可以避免這種情況:

// elsewhere in boobooland
Array.prototype[1] = `fool!`
// and in my code
const a = [ undefined, , ];
console.log(a.hasOwnProperty(1)) // false
// ? have some of that boobooland
複製程式碼

自從 hasOwnPropertyES3 提出後,使用這個方法,我們可以稱自己是一個 古典程式設計者。?

為什麼(真心話)

效能可以作為闡述孔的存在的原因,比如建立一個陣列 new Array(10000000),這裡並沒有 1 千萬的值被分配在陣列中,瀏覽器也不會儲存任何資料。

在遍歷中終止

我們驗證一下在 mapforEachfilter 這3種遍歷模式下的回撥情況。

forEach 將跳過所有的孔,也會耗時。

const a = new Array(100)
a[1] = 1
let i = 0
a.forEach(() => i++)
console.log(i) // 1 - the callback is never called
複製程式碼

mapforEach 一樣,跳過所有孔,但是也會在結果中 return 對應的孔。

const a = [ undefined, , true ];
const res = a.map(v => typeof v);
console.log(res) // [ "undefined", hole × 1, "boolean" ]
複製程式碼

當然,全是孔的陣列使用 map return 固定值,也不好使,仍然會 return 一個孔,對應的回撥函式並不執行。

new Array(10).map((_, i) => i + 1) // hole × 10
// not 1, 2, 3 … etc ?
複製程式碼

然後,filter 會移除對應的孔。

const a = [ undefined, , true ];
const res = a.filter(() => true);
console.log(res) // [ undefined, true ] - no hole ?
複製程式碼

在迴圈中進行

有兩種迴圈可以遨遊稀疏陣列,第一種是經典的迴圈,for,當然,whiledo/while一樣起作用。

const a = new Array(3);
for (let i = 0; i < a.length; i++) {
  console.log(i, a[i]) // logs 0…2 + undefined
}
複製程式碼

另外,ES6 的陣列方法,會將陣列的孔轉換為 undefined,這是因為在底層,陣列展開使用了 iterator 協議

這意味著在如果要拷貝陣列的時候,需要謹慎使用 ... 擴充套件運算,而使用 slice 方法拷貝。

const a = [ 1, , ];
const b = [ ...a ];
const c = a.slice();

expect(a).toEqual(b); // false
expect(a).toEqual(c); // true
複製程式碼

適用於稀疏陣列的情況

對比一下 Array.from({ length }) 的情況,可以看出耗時差距。

const length = 1000000; // 1 million ?
let time = new Date().getTime()
new Array(length); // ~5ms
console.log(new Date().getTime() - time)
time = new Date().getTime()
Array.from({ length }) // ~150ms
console.log(new Date().getTime() - time)
複製程式碼

總結

  • 分隔符尾部元素會被忽略
[ 1, 2, 3, ] // no hole at the end, just a regular trailing comma
複製程式碼
  • 分隔符中間為孔
[ 1, , 2 ] // hole at index(1) aka empty
複製程式碼
  • 檢測孔使用 array.hasOwnProperty(index)
[ 1, , 2 ].hasOwnProperty(1) // false: index(1) does not exist, thus a hole
複製程式碼
  • 遍歷方法,如 mapforEachevery 不會回撥孔
const a = new Array(1000);
let ctr = 0;
a.forEach(() => ctr++);
console.log(ctr); // 0 - the callback was never called
複製程式碼
  • map 返回的新陣列會包含對應的孔
[ 1, , 2 ].map(x => x * x) // [ 1, <empty>, 4 ]
複製程式碼
  • filter 返回的陣列會移除孔
[ 1, , 2 ].filter(x => true) // [ 1, 2 ]
複製程式碼
  • keysvalues 返回的迴圈方法會遍歷到孔,包含但不限於 for key of array.keys())
const a = [ `a`, , `b` ];
for (let [index, value] of a.entries()) {
  console.log(index, value);
}
/* logs:
- 0 `a`
- 1 undefined
- 2 `b`
*/
複製程式碼
  • 陣列展開運算 [...array] 會轉換孔為 undefined,這當然會增加記憶體消耗,影響效能
[...[ `a`, , `b` ]] // [`a`, undefined, `b`]
複製程式碼
  • 龐大的資料建立非常迅速,比 Array.from 快多了
const length = 10000000; // 10 million
new Array(length); // quick
Array.from({ length }) // less quick
複製程式碼

相關文章