彻底掌握Set & Map

499 阅读5分钟

阵列与物件的进化 - Set & Map

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"}

Map 的顺序?

Object 跟 Map 很像,但在 key/value 的顺序方面,Object 的 key/value 基本上还是会根据设置 (set) 的顺序,但是根据MDN的说法,它的排序还是有一些复杂因素影响 (起码如果数字的 key 就不会按照 set 顺序),所以尽量不要相信 Object key 的顺序 XD

但 Map 就不一样了,会按照 key/value 的 set 顺序,可以透过Object.keysObject.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 比较好)