iT邦幫忙

2021 iThome 鐵人賽

DAY 5
2
Modern Web

Javascript 從寫對到寫好系列 第 5

Day 5 - 陣列與物件的進化 - Set & Map

前言

在看完前三天的 Array & Object 組合技,感覺只要這兩個東西練得夠熟,應該就可以 cover 大部分關於資料儲存結構的問題了。

或許一般教學 Javascript,的確是到這就差不多了,但這系列文是不斷邁向更「好」的,所以我們再讓自己多學一點!

因為你看到了嗎?Array 跟 Object 他們也在進化啦!

Set

Set 中文可以翻成「集合」,數學上的那個「集合」,所以 Set 可以比較容易做到「交集」、「聯集」、「差集」等動作。

Set 的重點是「元素不可重複」,語法像下方這樣,需要用 new 關鍵字來產生一個 Set,如果有初始值則帶入一個 Array 到參數內:

new Set([iterable]);

與 Array 的差距

Set 可以說是 Array 的進化版,但主要有兩點跟 Array 不同:

  1. 元素不可重複
  2. 沒有 index 可以存取(但,Set 仍然是有順序的)

聽起來這進化幅度有點小欸,第二點是不是退化了啊?而且就只是從「可重複」變成「不可重複」而已,那我也可以在 Array 裡面判斷是否重複呀:

const arr = ['Susan', 'Allen', 'Jack'];
const newItem = 'Allen';

if (!arr.includes(newItem)) {
    arr.push(newItem);
}

是的,看起來就是多了個 if 判斷而已,但如果我有很多個地方都要做 push 的動作,甚至不只是 push,連 unshiftsplice 等 method 也都會有新增元素到陣列的動作,那我的 if 判斷式不就加不完了?

就算真的都克服了上述的麻煩,寫出來的程式也的確是「對」的,但如果換另一個工程師接手維護,光用看的也沒辦法立刻知道,哪個 Array 會重複,哪個 Array 不會重複。

上述的感覺就像是:我接手了一份 code,裡面全都用 let 來宣告變數,完全沒有 const。程式一定能跑沒問題,但我就完全不知道哪些變數會變動、哪些不會。

也就是說,Array 這個進化其實不是功能上的進化,而是可讀性與可維護性的進化。

✔ 過濾陣列中重複的元素

最簡單的用法,就是把一個帶有重複元素的陣列,丟進去 Set,然後再透過 spread oparator 轉回 Array:

const arr = ['Susan', 'Allen', 'Jack', 'Allen'];
const arrSet = new Set(arr);
const uniqueArray = [...arrSet];

console.log(uniqueArray);

執行結果

['Susan', 'Allen', 'Jack']

✔ 配送不重複地址

比如我是個送貨員,手上有今天所有要配送的訂單,但有些地址是重複的,我想要到一個地點就把當地所有貨送完。

const arr = [
    { customer: 'Allen', address: '新北市政府'},
    { customer: 'Susan', address: '台大醫院'},
    { customer: 'Jack', address: '板橋高鐵站'},
    { customer: 'Alice', address: '新北市政府'},
];
const arrSet = new Set(arr.map(item => item.address));

console.log(`今天總共要送 ${arrSet.size} 個地方`);
console.log(Array.from(arrSet).join('、'));

執行結果

3
新北市政府、台大醫院、板橋高鐵站

Set 有順序嗎?

大哉問,我第一次接觸到 Set 的時候,雖然知道這是為了模擬數學上的「集合」,但,「集合」應該是沒有順序的才對吧?

關於順序的部分,有發現嗎?上面兩個範例中,雖然我都沒有辦法使用像是 arrSet[0] 之類,透過 index 存取的語法,看起來好像 Set 沒有順序的概念,但當我透過 [...arrSet] 或者 Array.from(arrSet) 轉換成 Array 時,它的順序都跟原本一樣。

代表 Javascript 的 Set 其實是有紀錄順序的(可參考 MDN,但很不明顯,再補充個 StackOverflow):

  • 根據 new Set(arr)Array.from(arr) 的 arr 順序
  • 根據 Set.add() 新增的元素順序

因此,雖然 Set 比 Array 少了 index 的操作,但卻幫我們處理掉「重複」的問題,而這也是我們真正要使用它的原因,可以透過在 Array 跟 Set 之間切換(當然不要太頻繁),把 Array 最怕的重複問題解決掉。

補充:Set 如何判斷重複?

當我們使用 Set.add() 指令時,Javascript 背後是如何判斷有沒有重複的呢?

背後其實是使用嚴格相等(===)

也就是說,如果 Set 裡面放的是 non-primitive 的元素要特別小心,因為 non-primitive 是 by reference 來判斷,所以通常都會當作「不重複」處理(因為記憶體位址不同):

const arr = [
    { customer: 'Allen', address: '新北市政府'},
    { customer: 'Allen', address: '新北市政府'},
];

const arrSet = new Set(arr);

console.log(arrSet);

arrSet.add({ customer: 'Allen', address: '新北市政府'});

console.log(arrSet);

執行結果

Set(2) {
    { customer: 'Allen', address: '新北市政府'}, 
    { customer: 'Allen', address: '新北市政府'}
}
Set(3)) {
    { customer: 'Allen', address: '新北市政府'}, 
    { customer: 'Allen', address: '新北市政府'}, 
    { customer: 'Allen', address: '新北市政府'}
}

另外,NaN 和 undefined 都可以被放置在 Set 中(儘管 NaN !== NaN)。

Map

Map 跟 Object 很像,使用起來最主要的差別是:

  • Object 的 key 是 string/Symbol;而 Map 的 key 可以是任何類型
  • Object 跟 Map 的順序有微妙的不同(詳見下方討論)
new map([iterable]);

Map 的 key 可以用任何類型

雖然 key 可以用 Object、Array、甚至 function,但因為 key 不能重複,仍然會回到「如何判斷 key 有沒有重複」的問題。

答案跟前面介紹的 Set 非常像,就是 嚴格相等(===) 來判斷,所以如果要用 non-primitive 來當 key 要特別小心:

const personMap = new Map();
personMap.set({
    height: 173,
    weight: 63
}, 'Joey');

// 可能。。。有個身高體重跟 Joey 一模一樣的 Susan
personMap.set({
    height: 173,
    weight: 63
}, 'Susan');

console.log(personMap.size);
console.log(personMap);

執行結果

2
Map(2) {{…} => "Joey", {…} => "Susan"}

補充,上面的範例也可以改寫成這樣,直接給初始值,初始值要是 Array of Array:

const personMap = new Map([
    [{ height: 173, weight: 63 }, 'Joey'],
    [{ height: 173, weight: 63 }, 'Susan']
]);

console.log(personMap.size);
console.log(personMap);

Map 的順序?

Object 跟 Map 很像,但在 key/value 的順序方面,Object 的 key/value 基本上還是會根據設置(set)的順序,但是根據 MDN 的說法,它的排序還是有一些複雜因素影響(起碼如果數字的 key 就不會按照 set 順序),所以盡量不要相信 Object key 的順序XD

但 Map 就不一樣了,會按照 key/value 的 set 順序,可以透過 Object.keys / Object.values / Object.entries 看到這些跡象。

比如說,我們要幫學生設定的 key/value,分別用 座號/名字:

const studentObj = {};
studentObj['22'] = 'Joey';
studentObj['33'] = 'Allen';
studentObj['11'] = 'Susan';

console.log(Object.keys(studentObj));

const studentMap = new Map();
studentMap.set('22', 'Joey');
studentMap.set('33', 'Allen');
studentMap.set('11', 'Susan');

console.log(studentMap.keys());

執行結果

["11", "22", "33"]
MapIterator {"22", "33", "11"}

Map 適合的情境

Map 跟 Object 很像,一些基本的情境其實兩邊可以互相取代,但比較適合 Map 的情境是:

  • 需要用到非 string/Symbol 的 key
  • 需要有穩定可依循的 key 順序
  • 規模較大、較常修改的 key/value (效能上 Map 比較好)

結語

Set 跟 Map 算是比較特殊用途才會出現的,因此說真的就算直接無視它們也是活得好好的,不過多學一點總是好的,因為當使用 Object、Array 的過程中覺得「好像哪裡卡卡的」的時候,就會有一道光降臨到頭頂上,會有個長著翅膀的人飛過來問你,「要不試試 Set 或 Map?」

獨一無二的個體
即便外表相似
靈魂仍在不同的地址

參考資料

Set MDN
Map MDN
es6-map-vs-object-what-and-when
why-does-js-keep-insertion-order-in-set


上一篇
Day 4 - Object 物件組合技
下一篇
Day 6 - Function 時空旅行 (1) - 參數優化
系列文
Javascript 從寫對到寫好30

2 則留言

1
TD
iT邦新手 5 級 ‧ 2021-09-20 22:33:40

Set 可以說是 Array 的進化版

印象中 Set 的背後的實作還是 hash table,所以在存取資料的速度上也比 array 還要快(這也是進化的一部分)

ycchiuuuu iT邦新手 5 級 ‧ 2021-09-21 11:09:31 檢舉

原來如此,我這次沒有研究到這一塊(筆記),感謝補充!

2
TD
iT邦新手 5 級 ‧ 2021-09-20 22:36:03

我發現每篇文章最後面都有一小段的 (詩句?)

天平的兩端
藏著各自的風景
寫著各自的故事

漫天星點
在每一個連結與跨越
化作指引的星輝

自己寫下自己的屬性
自己決定自己的命運

獨一無二的個體
即便外表相似
靈魂仍在不同的地址

pjchender iT邦新手 4 級 ‧ 2021-09-21 00:51:01 檢舉

太厲害了!

ycchiuuuu iT邦新手 5 級 ‧ 2021-09-21 11:08:36 檢舉

這位看倌眼睛真利!而且還幫我全部連在一起了~

我也說不上這是(殘缺的)新詩還什麼,單純是從我的理解,轉換成另一種視角來看那天的主題,或許等 30 天過去,再來試試看能不能從新詩反推回去是哪一天的主題 XDDD (怎麼感覺有點像 Map)

Jen iT邦新手 5 級 ‧ 2021-09-21 14:59:39 檢舉

靈魂仍在不同的地址

精神分裂XD

我要留言

立即登入留言