iT邦幫忙

2022 iThome 鐵人賽

DAY 30
0

Abstract

我是阿傑,今天是第 30 天了!

如今也都好過了

雖然過程總是辛苦,但留在腦中的東西才是可以讓人好過的真正原因,我們今天就來聊聊什麼是 陣列 吧!

我想每個學過 JavaScript 的人都聽過這句話 - JavaScript 的陣列是一個特殊物件! 這句話聽起來理所當然,但當時的我卻始終無法理解,為什麼陣列不是陣列,它要是一個特殊物件?最後我試著從不同角度切入,得到了一些結論,我們就來看看吧:

什麼是陣列?

在談陣列前,我們先來談談什麼是集合!假設我們有一堆資料散落在各地,例如以下:

30.3

這邊有各種類型的資料,包括數字、字串、變數等等,它們看起來很讓人頭痛,因為它們彼此毫無關聯且沒有邏輯地散落一地。

那如果我嘗試挑出我認為有用的資料出來呢?也就是我隨意抓了一些資料放在一起,便創造出了一個資料聚集體 - 稱為集合,它可能會長這樣:

30.1

這邊一共有 4 筆資料,分別為 2 個數字、1 個字串跟 1 個變數,它們本身其實互不相干 (就算都是數字),直到我畫了個圈圈把我想要的資料個別抓了進來,然後告訴大家這是個 personalFile !因此你可以在這個 personalFile 的集合裡看到跟個人檔案相關的資料,而這件事是由我賦予的,並非資料本身!

但你會發現這個集合就真的只是一個集合!它並不好用,因為你並不知道裡面的每個資料代表的是什麼?例如我們無法分辨數字 10 跟數字 25 分別代表什麼!尤其在資料非常龐大、甚至出現了其他相同類型的集合時,這件事會變得更為棘手!

為了解決這個問題,我們索性給了這些資料各別的標籤,你可以稱這個標籤為屬性的鍵 - keyfield。此時,這個集合看起來會像這樣:

30.2

你會發現,這個集合馬上變成我們印象中的 JavaScript 物件、 ECMAScript 中的 Record 或者資料庫中的 table!如果將其程式碼化:

const personalFile = {
	id: 10,
	name: 'Pedro',
	age: 25,
	address: address_1
}

現在每筆資料都有了自己的歸屬,我們可以利用這些標籤找到集合中相對應的資料,而標籤跟資料的組合 (pair) 就可以稱為一個屬性,你可以稱標籤為 key、資料為 value

而這些標籤其實也可以是數字型態的字串,例如 - 012,所以我們也可以把這個集合變成這樣:

30.4

你會發現它突然看起來像 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 的陣列又是什麼?

從以上的線索,會發現 JavaScript 的陣列看起來真的就像是物件,似乎只是寫法不同!

但還記得我們在 Day 4 曾經說過 :

在 ECMAScript 中,一個物件的實際語義 (actual semantic) 是透過 internal methods 來定義的...

也就是說一個物件的種類是由其內部方法 (internal method) 所定義的,而陣列之所以會是陣列,便是因為它的 [[DefineOwnProperty]] 這個內部方法跟一般物件不同!

我們可以嘗試在 ECMAScript 搜尋看看這個內部方法,會發現它出現好幾個地方,例如在一般物件的 DefineOwnProperty - ordinary object、或者在陣列特殊物件的 DefineOwnProperty - Array exotic object,它們都可以定義自身的屬性,但在定義時卻有著不同的行為,而這個差異導致他們成為了不同類型的物件。

如果你有試著點進連結,會看到陣列的 [[DefineOwnProperty]] 比一般物件的 [[DefineOwnProperty]] 多了一道程序,也就是以下這串:

30.5

我們來試著分析一下!

這邊會看到如果設置的屬性為 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 而非實際存在的元素
  • empty slot 是不存在的元素,它只是陣列為了維持 length 的長度而填充的產物
  • 陣列的咩色幾乎都被做成通用的,表示類陣列跟類似陣列的物件都可以使用
  • 咩色除了好用還帶給程式碼更高的可讀性
  • 善用咩色可以減少很多不必要的變數宣吿
  • 它讓程式碼變得很美用起來很開心

後記

這是這系列的最後一篇文章,謝謝 Chris 推薦並指導我閱讀 ECMAScript,得到了非常非常多的啟發並解開了我心中超多的迷惑,也同時希望帶給觀看文章的你一點幫助!

最後,希望大家可以開心地使用各種咩色,體驗它帶給你的便利,祝大家歸剛沒煩惱。


上一篇
Day 29 咩色用得好 - Array.prototype.sort (part - 2)
系列文
咩色用得好,歸剛沒煩惱 - 從 ECMAScript 偷窺 JavaScript Array method30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中
0
安揪拉
iT邦新手 4 級 ‧ 2022-10-15 23:25:17

太用心了吧~恭喜完賽~
先留言明天再看哈哈哈(躺下)

0
Vic
iT邦新手 3 級 ‧ 2022-10-16 20:05:32

恭喜完賽/images/emoticon/emoticon12.gif

0
Chris
iT邦新手 4 級 ‧ 2022-10-17 13:03:51

恭喜,又有一個神作出現了

我要留言

立即登入留言