我是阿傑,今天是第 30 天了!
雖然過程總是辛苦,但留在腦中的東西才是可以讓人好過的真正原因,我們今天就來聊聊什麼是 陣列 吧!
我想每個學過 JavaScript 的人都聽過這句話 - JavaScript 的陣列是一個特殊物件! 這句話聽起來理所當然,但當時的我卻始終無法理解,為什麼陣列不是陣列,它要是一個特殊物件?最後我試著從不同角度切入,得到了一些結論,我們就來看看吧:
在談陣列前,我們先來談談什麼是集合!假設我們有一堆資料散落在各地,例如以下:
這邊有各種類型的資料,包括數字、字串、變數等等,它們看起來很讓人頭痛,因為它們彼此毫無關聯且沒有邏輯地散落一地。
那如果我嘗試挑出我認為有用的資料出來呢?也就是我隨意抓了一些資料放在一起,便創造出了一個資料聚集體 - 稱為集合,它可能會長這樣:
這邊一共有 4 筆資料,分別為 2 個數字、1 個字串跟 1 個變數,它們本身其實互不相干 (就算都是數字),直到我畫了個圈圈把我想要的資料個別抓了進來,然後告訴大家這是個 personalFile
!因此你可以在這個 personalFile
的集合裡看到跟個人檔案相關的資料,而這件事是由我賦予的,並非資料本身!
但你會發現這個集合就真的只是一個集合!它並不好用,因為你並不知道裡面的每個資料代表的是什麼?例如我們無法分辨數字 10
跟數字 25
分別代表什麼!尤其在資料非常龐大、甚至出現了其他相同類型的集合時,這件事會變得更為棘手!
為了解決這個問題,我們索性給了這些資料各別的標籤,你可以稱這個標籤為屬性的鍵 - key
或 field
。此時,這個集合看起來會像這樣:
你會發現,這個集合馬上變成我們印象中的 JavaScript 物件、 ECMAScript 中的 Record 或者資料庫中的 table!如果將其程式碼化:
const personalFile = {
id: 10,
name: 'Pedro',
age: 25,
address: address_1
}
現在每筆資料都有了自己的歸屬,我們可以利用這些標籤找到集合中相對應的資料,而標籤跟資料的組合 (pair) 就可以稱為一個屬性,你可以稱標籤為 key
、資料為 value
。
而這些標籤其實也可以是數字型態的字串,例如 - 0
、1
、2
,所以我們也可以把這個集合變成這樣:
你會發現它突然看起來像 JavaScript 的陣列或者 ECMAScript 的 List ,因為它擁有了陣列的第一個特性 - 從 0 開始且非負數的索引 (array index),如果將其程式碼化:
const personalFile = [10, 'Pedro', 25, address_1]
console.log(personalFile.length)
// 4
const anotherPersonalFile = {
0: 10,
1: 'Pedro',
2: 25,
3: address_1,
length: 4
}
除此之外,你應該還發現這個集合多了一個屬性 - length
,這個 length
便是陣列中最重要的另一個特性,而也正是這個 length
,讓陣列成為了 JavaScript 中的特殊物件 (exotic object)。
從以上的線索,會發現 JavaScript 的陣列看起來真的就像是物件,似乎只是寫法不同!
但還記得我們在 Day 4 曾經說過 :
在 ECMAScript 中,一個物件的實際語義 (actual semantic) 是透過 internal methods 來定義的...
也就是說一個物件的種類是由其內部方法 (internal method) 所定義的,而陣列之所以會是陣列,便是因為它的 [[DefineOwnProperty]]
這個內部方法跟一般物件不同!
我們可以嘗試在 ECMAScript 搜尋看看這個內部方法,會發現它出現好幾個地方,例如在一般物件的 DefineOwnProperty - ordinary object、或者在陣列特殊物件的 DefineOwnProperty - Array exotic object,它們都可以定義自身的屬性,但在定義時卻有著不同的行為,而這個差異導致他們成為了不同類型的物件。
如果你有試著點進連結,會看到陣列的 [[DefineOwnProperty]]
比一般物件的 [[DefineOwnProperty]]
多了一道程序,也就是以下這串:
我們來試著分析一下!
這邊會看到如果設置的屬性為 length
或 陣列索引 (array-inex) 便會有特殊的處理,如果都不是就走會一般的物件屬性設置流程。
當新增的屬性為 length
時,會使用 ArrarySetLength()
這個抽象操作來設置 length
,因為 ArraySetLength()
這個抽象操作挺複雜的,這邊我直接劃重點:
length
大於等於舊的 length
,便將其改為新的 length
length
小於舊的 length
時,除了將其改為新的 length
之外,還會將所有大於等於 length
的索引屬性給刪除,看起來會像這樣:const array = [0, 1, 2, 3, 4]
array.length = 3
// 此時陣列中索引大於等於 3 的屬性都將被刪除
console.log(array)
// [0, 1, 2]
而當新增的屬性為 陣列索引 時,除了將其新增外,還會檢查這個索引是否大於 length
,如果是的話便會重新設置 length
的長度,其值會是這個 索引 + 1,看起來會是這樣:
const array = [0, 1, 2, 3, 4]
array[5] = 5
// 當新增索引屬性時,length 會重新設置
// 其值會是 5 + 1
console.log(array.length)
// 6
看到這邊是不是發現了一個以前從來不會注意的事?那就是陣列的 length
跟 索引屬性 是有超能力的:
length
時,陣列中的 索引屬性 會同時被更動length
也會被更動這些都是我們在對一般物件設值時不會發生的!也就是說陣列跟一般物件最根本的區別就在於其 length
跟 索引屬性 的 setter 有做過特殊處理,使它們擁有能在設值時同步改動其他屬性的超能力。
既然我們知道了 length
有超能力 - 它可以刪除陣列中大於等於它的索引屬性,那我們就來談談稀疏陣列 (sparse array) 吧!
稀疏陣列是一個非常神奇的存在,說它是新人殺手也不為過,我們先來看看它什麼時候會出現:
const array1 = Array(2)
console.log(array1)
// [empty × 2]
console.log(array1.length)
// 2
const array2 = [0, 1, 2]
array2.length = 5
console.log(array2)
// [0, 1, 2, empty × 2]
console.log(array2.length)
// 5
上述兩個狀況都產生了 empty slot,如果我們再結合前面所提到的陣列特性,就會發現一個驚人的事實 - 也就是真正控制陣列長度的並非 實際存在的元素,而是陣列的 length
屬性!
如果你去存取這些 empty slot,拿到的會是 undefined,也不會發生任何錯誤,那它們到底存不存在?我們來驗證一下:
const array1 = Array(2)
const array2 = [0, 1, 2]
array2.length = 5
console.log('0' in array1)
// false
console.log(array1.hasOwnProperty('0'))
// false
console.log('3' in array2)
// false
console.log(array2.hasOwnProperty(3))
// false
從結果來看這些索引屬性通通不存在,這也驗證了它拿到的 undefined 是一般物件找不到屬性時所回傳的 undefined,而非元素存在但其值為 undefined。
因此我們可以推斷 length
多少,陣列就會多長,如果實際元素不足長度,那陣列也不管它,length
說多少就是多少,而陣列為了維持這個長度,將不存在的屬性使用 empty slot 填充 (做假帳的概念?),雖然這個填充看似合理,但其實這可以視為一種直接操作 length
的副作用,也可以說是我們在操作陣列的途中,沒有依據新增或刪除的元素數量做出正確的 length
更新。
當我得知 length
的真相時,我其實是滿頭問號的,我無法理解為什麼不依照陣列實際存在的元素去計算長度就好,而要聽一個可以隨心所欲想改就改的 length
,這不是很不靠譜嗎?
還好當我遇到這個人生難題的時候,Chris 正好在旁邊閒閒地走來走去,我趕緊求救,祈求可以打通任督二脈!而此時我猜不少人應該早就知道答案了 - 沒錯,就是 時間!
JavaScript 選擇了使用 1 個屬性的空間來換取每次讀取的計算時間,也就是答案早就存在陣列裡面了,不需要等我們讀取 length
才去計算陣列中有多少元素,而這個答案也是需要計算的,因此 JavaScript 把這個計算分散到所有可能修改到陣列的操作裡,例如設置陣列的元素、或者會影響長度的 Array method ,像 push()
跟 pop()
都可以看到在操作結束之前都會重新設置 length
。
這樣一來,每次當我們想要知道陣列的長度時,只需要讀取存在陣列 length
屬性中的答案即可!但這也表示每次變動陣列時,我們都應該要正確地設定新的 length
,如果我們沒事直接去更動length
這個屬性,就很有可能導致一些非預期的副作用發生。
最後,我總結一下 JavaScript 陣列的重點:
length
兩個魔法屬性length
而非實際存在的元素length
的長度而填充的產物這是這系列的最後一篇文章,謝謝 Chris 推薦並指導我閱讀 ECMAScript,得到了非常非常多的啟發並解開了我心中超多的迷惑,也同時希望帶給觀看文章的你一點幫助!
最後,希望大家可以開心地使用各種咩色,體驗它帶給你的便利,祝大家歸剛沒煩惱。