前言

這兩天會來講Set/Map,這是ES6後才新增的兩種資料結構,主要是用來處理一些arrayobject沒辦法完全顧及的領域,也因如此,其實許多地方跟arrayobject非常相似,那今天就從set開始吧!

set是集合的意思,在JavaScript裡是唯一值的集合,意思就是說,set裡面的每一個值都只能出現一次。

什麼是集合?

set 的中文翻譯是集合的意思,在 JavaScript 裡這個語法是唯一值的集合,而數學上的集合定義,去爬了維基百科,得到以下結論:

指具有某種特定性質的事物的母體,集合裡的事物稱作元素。

下圖就是一個有一些多邊形的集合。關於集合我的理解是可以把它當作一堆類型差不多的東西,而在JavaScript就是一堆值,而在array確實就只有一大堆值,跟Set的差別就在於,JS 的Set值是獨一無二的。

出處: https://zh.wikipedia.org/zh-tw/%E9%9B%86%E5%90%88_%28%E6%95%B0%E5%AD%A6%29

set常用方法與屬性

  • new Set() => 創建一個set
  • add() => 往set裡面新增一個元素
  • delete() => 從set裡面刪除一個元素
  • has() => 判斷set有沒有存在指定的元素
  • clear() => 刪除set裡面所有東西
  • size => 回傳整個set裡面元素的種類量

以下簡單實作一次:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const vicSet = new Set([1, 2, 2, 3]);
console.log(vicSet); // Set(3) { 1, 2, 3 }

// ↑一開始創建出來,set裡面是空的

set.add(4);
console.log(vicSet); // Set(4) { 1, 2, 3, 4 }

// ↑幫set新增加了4這個元素進去

set.delete(1);
console.log(vicSet); // Set(3) { 2, 3, 4 }

// ↑把1這個元素從set完整的刪除

console.log(vicSet.has(1), vicSet.has(2)); // false true

// ↑判斷set裡面有沒有這些指定的元素

console.log(vicSet.size); // 3

// ↑這個屬性將會找出set裡面元素的種類

set.clear();
console.log(vicSet); // Set(0) {}

// ↑把map裡面所有東西都清空

拿不出的值,跟沒有 index 的Set

我嘗試從set裡面拿到值,但發現拿不出來,why?

1
2
3
4
5
let vicSet = new Set([1, 2, 3, 4, 5]);

console.log(cicSet[0]);

// undefined

因為set實際上就沒有 index,所以沒有辦法從set獲取值,可能也沒有必要,不然就會發明出一個叫做 get()的方法對吧?

我想只需要知道,某一個值是不是還存在於集合裡面,所以有 has()的方法可以檢查,畢竟set的特點是獨一無二的值,都是唯一的,順序沒有意義。

沒有必要在set獲取值的原因是,如果目的是為了按照順序存取值,然後去獲取它,**那就應該要去用array,而不是Set**。

可以想像成是,假如我有五顆蘋果要塞入一個盤子集合裡面:

set的方式 => 盤子裡有一個種類 - 蘋果 (不知道誰先誰後)。
array的方式 => 蘋果陸陸續續地進到了盤子裡面 (知道誰先誰後)。

1
2
3
4
5
6
const plate = ["apple", "apple", "apple", "apple", "apple"];
console.log(plate[3]); // apple, 第四顆進去的蘋果

const plate2 = new Set();
plate2.add(["apple", "apple", "apple", "apple", "apple"]);
console.log(plate2); //Set(1), 一個種類 蘋果

那我又想要有set的特性,又想要有array的 index 使用方式要怎麼做?

set把資料都處理完後,再轉成array,就可以了。

1
2
3
4
5
let vicSet = new Set([1, 2, 3, 4, 5]);

let arrVicSet = Array.from(vicSet);

console.log(arrVicSet[0]); // 1

Set 可以被迭代

這代表著使用 for..offorEach 来遍歷 Set
基本上跟array都一樣,因為都是 Iterable(可迭代)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let set = new Set(["apple", "vic", "book"]);

for (const order of set) {
console.log("for..of 方法: " + order);
}

set.forEach((order) => {
console.log("forEach: " + order);
});

// for..of 方法: apple
// for..of 方法: vic
// for..of 方法: book
// forEach: apple
// forEach: vic
// forEach: book

Set迭代方法

  • set.keys() => 遍歷 set
  • set.values() => 遍歷 set
  • entries() => 遍歷 set 後再返回一個實體
1
2
3
4
5
6
7
const vicSet = new Set([1, 2, 2, 3, 4, 5]);

console.log(vicSet.keys()); //[Set Iterator] { 1, 2, 3, 4, 5 }

console.log(vicSet.values()); //[Set Iterator] { 1, 2, 3, 4, 5 }

console.log(vicSet.entries()); //[Set Entries] { [ 1, 1 ], [ 2, 2 ], [ 3, 3 ], [ 4, 4 ], [ 5, 5 ] }

Set 實際上使用時機=> 刪除 arr 的重複值

這邊會用到一個概念 Spread Operator(展開運算符)。

Spread Operator

es6 才出來的一個新語法,來介紹一下它的作用。
其實就是展開的概念,看下面我寫的簡單範例應該就能了解。

1
2
3
4
5
const A = [4, 5, 6];

const B = [1, 2, 3, ...A];

console.log(B); //[ 1, 2, 3, 4, 5, 6 ]

...在當Spread Operator時,後面一定接著一個陣列,然後功用就是會把這個陣列給展開,像是上面範例那樣,常常用於來連接陣列,但是還有另外一種方法。

只要...前面沒東西時,後面也是會展開,但因為前面沒東西,就像是直接複製的感覺,所以也可以來當作陣列的淺拷貝(淺拷貝意思是改 arr2 不會影響到 arr)。

1
2
3
4
const arr = [1, 2, 3];
const arr2 = [...arr];

console.log(arr2); //[1, 2, 3]

方法一

使用剛剛前面講到的Spread Operator,搭配set的特性,先把陣列裡面的值都變成set獨一無二的值之後,再用Spread Operator複製起來到一個新的陣列,大功告成。

1
2
3
4
5
6
7
8
9
function unique(arr) {
return [...new Set(arr)];
}

let values = [1, 2, 2, 2, 3, 4, 4, 4, 5, 5];

let newValues = unique(values);

console.log(newValues); // [ 1, 2, 3, 4, 5 ]

方法二

首先要先了解Array.from是什麼,在這裡要知道的是它可以把set的東西轉成陣列,所以一樣是先把陣列裡面的值都變成set獨一無二的值之後,再轉成一個新的陣列,完成。

1
2
3
4
5
6
7
8
9
function unique(arr) {
return Array.from(new Set(arr));
}

let values = [1, 2, 2, 2, 3, 4, 4, 4, 5, 5];

let newValues = unique(values);

console.log(newValues); // [ 1, 2, 3, 4, 5 ]

WeakSet

最後來講講跟Set長得很像的WeakSet,其實不會差太多,但還是有一些差異。

像是會習慣在Set使用array但是在WeakSet使用時會報錯,這是因為在WeakSet只能增加object

1
2
3
let set = new WeakSet([1, 2, 3, 4, 5]);

console.log(set); //TypeError: Invalid value used in weak set

而跟WeakSet一樣的地方是會保留Set的一些方法:

  • add()
  • delete()
  • has()

但是size就沒辦法使用要特別注意。

Set要變成WeakSet的用意在哪裡?從字面意思上來看,weak是虛弱的意思,而虛弱的Set也沒辦法讓人直覺想到東西。

從 MDN 的解釋來看:

The WeakSet is weak, meaning references to objects in a WeakSet are held weakly. If no other references to an object stored in the WeakSet exist, those objects can be garbage collected.

來源:MDN

後面那段大意是說如果不存在對儲存在物件的其他引用,就可以把這些物件進行垃圾回收。

意思是如果有個物件使用 WeakSet之後,如果其他物件都不再引用這個物件,那就會有一個垃圾回收的機制,這個機制可能就是把這個對象所佔用的內存通通消滅掉,丟到垃圾桶。

這樣做有一些好處,首先變成Weak(弱)狀態的同時,可以想像成多了一個空間,可以拿來檢查,同時因為有Set的特性,獨一無二沒辦法重複。

主要用來判斷是跟否,而不是拿來做使用,所以是沒辦法拿出來獲取的。

1
2
3
4
5
6
7
8
9
10
11
12
13
let weakSet = new WeakSet();

let a = { a: 1 };
let b = { b: 1 };
let c = { c: 1 };

weakSet.add(a);
weakSet.add(b);

console.log(weakSet); // WeakSet { <items unknown> }

console.log(weakSet.has(a)); // true
console.log(weakSet.has(c)); // false

要直接清除掉只要再使用null就好。

1
2
a = null;
console.log(weakSet.has(a)); // false

簡單來說,WeakSet可以做的幾件事情:

  1. 幫其他地方的物件做一個額外的存取空間(集合)。
  2. 將物件弱連結(weak)到集合中 => add() 。
  3. 判斷這個物件有沒有成功有連結(佔用)了 => true、false。
  4. 從集合中刪除這個物件 => null 。

什麼情境會使用到呢?
個人覺得是很怕犯錯,需要避免錯誤時,可以在這個被「隔離」的集合內做檢查。

而 MDN 上面有提到說,可以用來檢測循環引用,這個我看起來也因為需要避免錯誤(迭代時涉及循環引用的錯誤),有興趣可以去研究看看。

reference

[1] MDN - Set
[2] W3Schools - JavaScript Sets
[3] The Modern JavaScript Tutorial - Map and Set
[4] JS 原力覺醒 Day29 - Set / Map
[5] PJCHENder -JavaScript 集合(Set)
[6] 前端工程師用 javaScript 學演算法 - 集合 Set
[7] 維基百科 - 集合 (數學)
[8] How to get index based value from Set