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 6

>>:  [想试试看JavaScript ] 各种事件处理 (二)

Day3 什麽是Git?

大家好,我是乌木白,今天我们开始讲我们这次铁人赛的第一个技能,就是Git啦!先和大家声明我是把我自...

[Day26] Flutter with GetX Push Notification

前面繁琐的设定请参考Mark飞大大的文章 然而在Mark飞大大文章的 5.接收阶段, 因为flutt...

Python课 第一式

print 输出指令 ?print Docstring: print(value, ..., sep...

Day 24 - PVE 修复 Loading initial ramdisk

今天笔者遇到一个问题,系统在开机时卡在 “Loading initial ramdisk”,用了一些...

每日挑战,从Javascript面试题目了解一些你可能忽略的概念 - Day11

tags: ItIron2021 Javascript 前言 昨天我们简单的带过IIFE,今天的主题...