阵列与物件的进化 - Set & Map
Set
Set 中文可以翻成「集合」,数学上的那个「集合」,所以 Set 可以比较容易做到「交集」、「联集」、「差集」等动作。
Set 的重点是「元素不可重复」,语法像下方这样,需要用 new 关键字来产生一个 Set,如果有初始值则带入一个 Array 到参数内:
new Set([iterable]);
与 Array 的差距
Set 可以说是 Array 的进化版,但主要有两点跟 Array 不同:
- 元素不可重复
- 没有 index 可以存取 (但,Set 仍然是有顺序的)
听起来这进化幅度有点小欸,第二点是不是退化了啊?而且就只是从「可重复」变成「不可重复」而已,那我也可以在 Array 里面判断是否重复呀:
const arr = ['Susan', 'Allen', 'Jack'];
const newItem = 'Allen';
if (!arr.includes(newItem)) {
arr.push(newItem);
}
是的,看起来就是多了个 if 判断而已,但如果我有很多个地方都要做 push
的动作,甚至不只是 push,连unshift
、splice
等 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.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 比较好)