前言
最近有空在看一本關(guān)于 JS 數(shù)據(jù)結(jié)構(gòu)和算法的書,里面有提到數(shù)組,卻對數(shù)組的基本概念輕輕帶過,雖然用了 JS 很久但是一直忙于需求業(yè)務(wù)的實現(xiàn)從未停下好好回視一下這個 既熟悉又陌生的朋友,于是查閱了一些資料,尤其是密集數(shù)組和稀疏數(shù)組的區(qū)別,意猶未盡之下,寫了這篇文章,以便更好地幫助理解書中的要點,稍顯淺顯,也有不足望各位提點。
什么是稀疏數(shù)組?
通常編程語言中(C、JAVA等)數(shù)組都是預(yù)先設(shè)定好長度的,他們的內(nèi)存占用是固定的,內(nèi)存地址是連續(xù)不間斷、緊密相連的,我們稱之為密集數(shù)組,好比 JS 中類型化數(shù)組(TypedArray)的 ArrayBuffer
,但我們這里著重要說的是通過 Array
創(chuàng)建的數(shù)組,它其實是個對象,當(dāng)我們通過 new Array()
創(chuàng)建一個數(shù)組時,它只是一個帶有 length
屬性的數(shù)組對象,我們可以像對象一樣去操作它的屬性:
let array = new Array(10)
// 下標(biāo)可以是任何的字符串,就像操作 Object 一樣
array.name = 'This is an Array.'
array['name'] = 'This is an Array.' // 等效 array.name
雖然可以像操作 Object 那樣為數(shù)組對象添加屬性,但是真正計入數(shù)組元素的只能是以整數(shù)類型的字符作為下標(biāo)去映射值的元素,同時會影響其長度屬性 length
,我們接上段代碼繼續(xù):
// 雖然之前賦上了一個新的屬性 name,但是其長度仍然輸出10,而非11
console.log(array.length) // 10
// 用整數(shù)類型下標(biāo)定義一個數(shù)組元素才會計入數(shù)組長度
array['0'] = 'first' // 下標(biāo)其實都是字符串
array[11] = 'eleventh' // 看起來這個下標(biāo)是一個數(shù)字,但是 JS 會自動把它轉(zhuǎn)為字符
console.log(array.length) // 12
長度屬性是可以手動變更的:
array.length = 100
console.log(array.length) // 100
數(shù)組對象本身可能不占用太多內(nèi)存,它只是包含了映射關(guān)系和長度,真正占用內(nèi)存的是元素所映射的目標(biāo),也就是“鍵值”中的“值”。在V8引擎里,JS 的數(shù)組得到進一步的優(yōu)化,其中有個概念叫 Holey(有孔洞的),也就是那些沒有被指明映射關(guān)系的空間,他們不占用內(nèi)存。如上看到,作為一門動態(tài)語言,我們隨時可以向這個數(shù)組對象里添加、修改元素映射,也能隨時改變其長度 length
。自然地,其元素所占的內(nèi)存地址也就無所謂固定連續(xù)了,我們稱這種數(shù)組為稀疏數(shù)組(Sparse Array)。
創(chuàng)建帶有孔洞的稀疏數(shù)組
- 使用 Array 構(gòu)造函數(shù):
let sparse = new Array(100)
- 通過字面量:
let sparse = []
// 用一個超過原始長度的下標(biāo)為元素賦值,也就為該元素創(chuàng)建了一個映射關(guān)系,同時改變了該數(shù)組的長度
sparse[99] = 'last'
console.log(sparse.length) // 100
// 用對象字面量書寫數(shù)組時,允許元素留空
sparse = [, , ,] // 這就創(chuàng)建了長度為3的空洞的數(shù)組
console.log(sparse.length) // 3
- 手動修改一個大于原數(shù)組長度的
length
值:
let sparse = ['red', 'green', 'blue'] // 三個元素的數(shù)組
sparse.length = 100 // 此時長度已是100,但有效元素仍然只有3個
刪除元素的映射
從數(shù)組中刪除某個元素可以使用 pop
、shift
、splice
方法,那如何解除元素與某個對象的映射關(guān)系呢?我們可以像操作對象一樣用 delete
,它不會刪除映射目標(biāo),僅僅是將元素和目標(biāo)對象的關(guān)聯(lián)斷開,從而形成一個孔洞,所以也不會改變這個數(shù)組的 length
。
let one = '壹'
let array = []
array[0] = one // 將數(shù)組的第一個元素指到對象 one,長度變?yōu)?1
console.log(array, array.length) // ['壹'], 1
delete array[0] // 刪除數(shù)組第一個元素的映射關(guān)系
console.log(array) // [空]
console.log(array.length) // 因為只是刪除了元素的映射,長度并沒有改變,仍然輸出 1
console.log(one) // one 的值并不會刪除,仍然保留,輸出 '壹'
現(xiàn)象
我們說到,JS 的數(shù)組本質(zhì)上是對象——由整數(shù)字符作為下標(biāo)與目標(biāo)值構(gòu)成映射關(guān)系的自帶長度屬性的對象,長度可以大于有效(已創(chuàng)建映射關(guān)系的)元素的個數(shù)。從映射狀態(tài)來看,完全映射的數(shù)組有著和傳統(tǒng)密集數(shù)組類似的表現(xiàn),而不完全映射的數(shù)組在生產(chǎn)中會有些特別,但又在情理之中。
- 訪問沒有映射關(guān)系的數(shù)組元素時,相當(dāng)于一個申明了卻沒有定義值的變量,所以會輸出
undefined
:
let sparse = new Array(10)
console.log(sparse[0]) // undefined
- 不完全映射的數(shù)組,在用
map()
、forEach()
等方法做遍歷時,它們只會遍歷已有映射關(guān)系的元素:
// 此時數(shù)組元素的映射關(guān)系一個都沒有創(chuàng)建,所以 forEach 不會有任何輸出
sparse.forEach((value, i) => {
console.log(i, value)
}) // nothing
// 根據(jù)下標(biāo)為最后一個元素賦值
sparse[sparse.length - 1] = 'tenth'
// 只會輸出已建立映射的元素 sparse[sparse.length-1] 的值 'tenth'
sparse.forEach((value, i) => {
console.log(i, value)
}) // 9, tenth
- 使用
for
語句是用數(shù)組的length
值作為循環(huán)依據(jù),它不會主動判斷當(dāng)前位置是否有映射值,所以當(dāng)循環(huán)體試圖通過下標(biāo)訪問沒有映射關(guān)系的位置時,會輸出undefined
:
for( let i = 0; i < sparse.length; i += 1){
console.log(i, sparse[i])
}
- 映射完全的數(shù)組,可以被所有常用數(shù)組方法遍歷:
// 一個完全映射的數(shù)組,這里的 undefined 是主動賦值的有效映射
let array = ['1', 'day', 'white', 'Jake', undefined, null, 0]
// 輸出每個元素,包括null、undefined、0
array.forEach((value, i) => {
console.log(i, value)
})
- 在做
some()
、every()
等操作時,不完全映射的數(shù)組的表現(xiàn)是特殊的,但也在情理之中,這取決于這些方法的設(shè)計,例如some()
,即便長度大于0,但因為其中沒有任何建立映射的元素,所以,相當(dāng)于給一個空數(shù)組([]
)做操作:
let sparse = new Array(10)
// 由于還沒有建立映射關(guān)系,所以 some 的回調(diào)也沒有觸發(fā),于是得到的結(jié)果依然是 false
console.log(sparse.some(item => !item)) // false
// 在所有元素都被賦值后,用同樣的回調(diào),some 的結(jié)果發(fā)生了變化
sparse.fill(false) // 賦值
console.log(sparse.some(item => !item)) // true
- 當(dāng)把長度設(shè)成小于實際元素個數(shù)的值時,會把超出長度的元素從當(dāng)前數(shù)組中剔除:
let array = ['one', 'two', 'three']
array.length = 1
console.log(array) // ['one']
稀疏數(shù)組的快速映射(強制創(chuàng)建映射關(guān)系)
只是讓數(shù)組中的映射元素個數(shù)與長度屬性相同,并不能改變其稀疏的特性:
let sparse = new Array(5)
// Array.apply
let array1 = Array.apply(null, sparse)
// Array.from方法
let array2 = Array.from(sparse)
// 解構(gòu)
let array3 = new Array(...sparse)
let array4 = [...sparse]
// 把所有元素映射為 undefined 了
array1.forEach((item, i) => {
console.log(i, item) // 輸出 undefined × 5
})
……
總結(jié)
以上是對 JS Array
數(shù)組的粗淺認知,在 JS 這門動態(tài)語言里,通過 Array
所創(chuàng)建的數(shù)組無所謂疏密,因為它們本質(zhì)上還是對象。在實際生產(chǎn)時,對于數(shù)據(jù)集合的操作應(yīng)當(dāng)盡量保持映射完全,避免不合預(yù)期的意外。