陣列arr1 = 1,1,2,2,3,3去重
放到Set去就好啦
題目看起來很簡單(一行 [...new Set(arr)] 就過了),但面試官真正想看的是你知不知道每種寫法的取捨:時間複雜度、能不能保留順序、遇到 NaN 跟物件參考會怎樣。把這幾個陷阱講清楚才是加分點。
方法一:Set + 展開(最簡潔,首選)
Set 本身就是「值唯一」的集合,丟進去再展開成陣列即可。
const arr1 = [1, 1, 2, 2, 3, 3];
const unique = [...new Set(arr1)];
console.log(unique); // [1, 2, 3]
// 等價寫法:Array.from 還能順便 map
const unique2 = Array.from(new Set(arr1));
- 時間複雜度 O(n):Set 內部用 hash,
add/has平均 O(1)。 - 保留順序:Set 依插入順序迭代,所以結果順序跟原陣列第一次出現的順序一致。
- 去重判定用 SameValueZero:跟
===幾乎一樣,但有一個關鍵差異 ——NaN被視為等於自己(見下方陷阱)。
方法二:filter + indexOf
indexOf 回傳「第一次出現的索引」,只有當前索引等於它時才保留 → 自然去掉後面的重複。
const unique = arr1.filter((item, index) => arr1.indexOf(item) === index);
console.log(unique); // [1, 2, 3]
- 時間複雜度 O(n²):
filter走 n 次,每次indexOf內部又是 O(n) 線性掃描。資料量大會明顯變慢。 - 陷阱:
indexOf用嚴格相等===比對,找不到NaN([NaN].indexOf(NaN)回傳-1),所以NaN會被全部濾掉而不是保留一個。要支援NaN改用findIndex(x => Object.is(x, item))。 - 好處:寫法直覺、不依賴 Set,相容極舊環境。
方法三:reduce
用一個 accumulator 陣列累積,沒看過的才 push。
const unique = arr1.reduce((acc, cur) => {
if (!acc.includes(cur)) acc.push(cur);
return acc;
}, []);
console.log(unique); // [1, 2, 3]
- 時間複雜度 O(n²):
includes每次都線性掃 acc。跟 filter+indexOf 同級。 - 真正的用途不是去重本身,而是去重的同時還要做別的事(轉型、聚合、計數),可以在 reduce 裡一次完成。單純去重沒必要繞 reduce。
includes用 SameValueZero,所以能正確處理NaN(跟 indexOf 不同這點常被問)。
方法四:Map(物件陣列依 key 去重)
原始型別用 Set 就夠了。但物件陣列不能直接丟 Set,因為 Set 比的是參考(reference),兩個內容一樣但不同 reference 的物件會被當作不同值:
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 1, name: 'Alice' }, // 內容相同但是「不同物件」
];
[...new Set(users)].length; // 3 —— Set 完全沒去到重!
正確做法:用 Map 以「業務上的唯一 key」(這裡是 id)當 key,後寫的會覆蓋先寫的,最後取 .values()。
const uniqueUsers = [...new Map(users.map(u => [u.id, u])).values()];
console.log(uniqueUsers);
// [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]
想「保留第一筆」而不是「最後一筆」new Map 在 key 重複時會用後面的值覆蓋,所以上面保留的是最後一個 id: 1。若要保留第一筆,先把陣列 reverse 再建 Map,或建 Map 時先判斷 if (!map.has(u.id)) map.set(u.id, u)。
陷阱整理(面試最愛問)
1. NaN 的去重行為
[...new Set([NaN, NaN])]; // [NaN] ✅ Set 認得 NaN
[NaN, NaN].filter((x, i) => [NaN].indexOf(x) === i); // [] ❌ indexOf 找不到 NaN
[NaN, NaN].reduce((a, c) => a.includes(c) ? a : [...a, c], []); // [NaN] ✅ includes 認得
| 比較方式 | 用在哪 | NaN === NaN | +0 / -0 |
|---|---|---|---|
=== (strict) | indexOf | false | 相等 |
| SameValueZero | Set、includes | true | 相等 |
| SameValue | Object.is | true | 不相等 |
重點記法:Set 跟 includes 都認得 NaN,只有 indexOf 不認得。
2. 物件去重比的是參考
如前述,Set / indexOf / includes 對物件都是比 reference,不是比內容。要按內容去重必須自己定義唯一鍵(單 key 用 Map;多 key 可用 JSON.stringify 當 key,但要注意 key 順序與不可序列化值的問題)。
3. 順序保留
四種方法全部都保留首次出現的順序(Set 依插入序、filter/reduce/Map 依遍歷序)。如果面試官追問「去重後要排序怎麼辦」,那是去重之後再 .sort(),兩件事分開做,不要混在去重邏輯裡。
總結對比
| 方法 | 時間複雜度 | 保留順序 | 處理 NaN | 適用場景 |
|---|---|---|---|---|
| Set + 展開 | O(n) | ✅ | ✅ | 原始型別,首選 |
| filter + indexOf | O(n²) | ✅ | ❌ 會濾掉 | 相容極舊環境 / 不想用 Set |
| reduce | O(n²) | ✅ | ✅ | 去重同時要做額外邏輯 |
| Map(依 key) | O(n) | ✅ | — | 物件陣列依業務 key 去重 |
一句話收尾:原始型別 [...new Set(arr)],物件陣列 [...new Map(arr.map(o => [o.key, o])).values()],其餘寫法知道原理跟陷阱就好。