JavaScript junior: xóa trùng mảng & chuỗi — Set, filter và tool dedupe【2026】
Junior viết JS: mặc định xóa trùng mảng bằng [...new Set(arr)]. Cần điều kiện riêng hoặc object theo id thì dùng Map / filter. Còn đang cầm list text mỗi dòng một mục (email, URL, SKU từ Excel) — đừng mất 15 phút viết snippet: dùng loại bỏ trùng trên trình duyệt (trim, không phân biệt hoa thường, giữ lần xuất hiện đầu). Tất cả chạy local, không upload.
Bạn mới vào team outsourcing hoặc đang thực tập. Mentor bảo: “list này có trùng, lọc unique rồi mới import.” Bạn mở Stack Overflow, thấy 5 cách khác nhau, rồi dán filter + indexOf vào production vì nhìn quen. List 8.000 email — máy đứng 2 giây, reviewer hỏi “sao không dùng Set?”
Bài này dành cho junior / sinh viên / freelancer Việt Nam làm việc với JavaScript hàng ngày: nắm mảng + chuỗi đủ để không bị kẹt ở bước dedupe, biết khi nào code, khi nào tool, và tránh 3 lỗi hay gặp khi xử lý tiếng Việt / email / emoji.
Bài viết này giúp bạn
- Nhớ 1 dòng chuẩn xóa trùng giá trị nguyên thủy
- Phân biệt Set vs filter vs Map bằng bảng và độ phức tạp
- Xử lý object theo id, không nhầm “Set unique object”
- Hiểu vài bẫy chuỗi: trim, hoa thường,
lengthvs grapheme - Biết lúc nào mở dedupe thay vì mở editor
Vì sao junior hay kẹt ở “xóa trùng”?
Ba tình huống lặp lại ở mọi dự án:
- API trả mảng — product id, slug, email — có duplicate vì join bảng hoặc cache
- Paste từ Excel / Google Sheet — mỗi dòng một giá trị, khoảng trắng thừa, hoa thường lẫn
- Log / crawl URL — cùng URL xuất hiện nhiều lần, cần unique trước khi gọi lại
Nếu bạn chỉ nhớ một câu: giá trị nguyên thủy → Set; object theo field → Map; text clipboard → tool.
Cách 1: Set — mặc định nên nhớ
const ids = [1, 2, 2, 3, 1, 4];
const unique = [...new Set(ids)];
// [1, 2, 3, 4] — giữ thứ tự lần xuất hiện đầu
Set không cho phép hai phần tử “bằng nhau” theo quy tắc SameValueZero (NaN cũng chỉ giữ một lần). Spread ... hoặc Array.from đưa Set về mảng.
Chuỗi cũng vậy:
const emails = [
"[email protected]",
"[email protected]",
"[email protected]",
];
const uniqueEmails = [...new Set(emails)];
// ["[email protected]", "[email protected]"]
Lưu ý: "[email protected]" và "[email protected]" là hai giá trị khác nhau. Set phân biệt hoa thường. Muốn gộp thì normalize trước:
const uniqueLower = [
...new Set(emails.map((e) => e.trim().toLowerCase())),
];
Cách 2: filter + indexOf — hiểu để tránh lạm dụng
const unique = arr.filter((value, index, self) => self.indexOf(value) === index);
Ý tưởng: chỉ giữ phần tử ở vị trí đầu tiên mà indexOf tìm thấy. Đúng với giá trị nguyên thủy, nhưng mỗi lần gọi indexOf quét lại cả mảng → O(n²).
| Kích thước n | Set (~O(n)) | filter+indexOf (~O(n²)) |
|---|---|---|
| 100 | gần như tức thì | gần như tức thì |
| 5.000 | vẫn mượt | bắt đầu chậm trên máy yếu |
| 50.000 | ổn trong vài ms–chục ms | dễ “đóng băng” tab vài giây |
Khi nào còn dùng filter? Khi điều kiện không phải === đơn giản, ví dụ giữ phần tử “đẹp nhất”:
// Giữ object có score cao nhất cho mỗi id (ý tưởng — không phải one-liner)
Còn “chỉ unique primitive” thì đừng chọn filter+indexOf chỉ vì quen mắt.
Cách 3: Object / Map theo khóa nghiệp vụ
const users = [
{ id: 1, name: "An" },
{ id: 2, name: "Bình" },
{ id: 1, name: "An (cập nhật)" },
];
// SAI kỳ vọng: Set giữ cả 3 vì reference khác nhau
[...new Set(users)].length; // 3
// ĐÚNG: unique theo id, giữ bản xuất hiện đầu
const byId = new Map();
for (const u of users) {
if (!byId.has(u.id)) byId.set(u.id, u);
}
const uniqueUsers = [...byId.values()];
// [{ id: 1, name: "An" }, { id: 2, name: "Bình" }]
Muốn giữ bản cuối (thường là bản mới hơn): bỏ if (!byId.has) và luôn byId.set(u.id, u).
So sánh nhanh — chọn gì?
| Cách | Phù hợp | Tránh khi |
|---|---|---|
| [...new Set(arr)] | number, string, boolean — code trong app | Object theo nội dung; cần ignore case mà chưa normalize |
| filter + indexOf | Hiểu thuật toán / môi trường cực cũ | List lớn; production hiện đại có Set |
| Map theo key | Unique object theo id / slug / email | Chỉ cần unique string đơn giản (dùng Set gọn hơn) |
| Tool dedupe | Text mỗi dòng một mục từ Excel/log | Đã là mảng trong runtime — nên code inline |
Chuỗi: 4 bẫy junior hay gặp
1. Khoảng trắng thừa
"[email protected] " và "[email protected]" khác nhau với Set. Luôn trim() trước khi dedupe nếu nguồn là paste.
2. Hoa thường email
Nhiều hệ thống coi email không phân biệt hoa thường. Normalize toLowerCase() trước khi unique — hoặc bật tùy chọn không phân biệt hoa thường trên dedupe.
3. length ≠ số ký tự người dùng thấy
"👋".length; // 2 (hai code unit UTF-16)
"ế".normalize("NFC").length; // thường 1
"ế".normalize("NFD").length; // có thể > 1 (base + dấu)
Với tiếng Việt: thống nhất NFC trước khi so khớp / unique nếu dữ liệu đến từ nhiều nguồn (form web, Excel, API).
4. Split chuỗi thành mảng rồi mới Set
const raw = "an,binh,an,chi";
const unique = [...new Set(raw.split(",").map((s) => s.trim()).filter(Boolean))];
Đừng new Set(raw) trên cả chuỗi — Set sẽ coi từng ký tự là phần tử.
Case study 1: Junior lọc product slug trước khi seed DB
Bối cảnh: Thực tập sinh nhận file JSON 2.000 sản phẩm; một số slug trùng vì export hai lần từ CMS.
const products = /* từ API */;
const seen = new Set();
const clean = [];
for (const p of products) {
if (seen.has(p.slug)) continue;
seen.add(p.slug);
clean.push(p);
}
console.log(products.length, "→", clean.length);
Bài học: Unique theo slug, không theo cả object. In số trước/sau để mentor review nhanh.
Case study 2: Freelancer nhận list email từ khách (Excel)
Khách gửi sheet, copy cột email vào chat. Có khoảng trắng, có Nguyen@... và nguyen@....
Làm bằng code mất thời gian mở file, viết script, chạy Node.
Làm bằng tool: mở Xóa trùng lặp, dán list, bật:
- Xóa khoảng trắng thừa trước khi so sánh
- Không phân biệt hoa thường
Kết quả giữ lần xuất hiện đầu, copy lại gửi khách hoặc import ESP. Dữ liệu không rời trình duyệt — phù hợp list khách hàng thật.
Quy trình 5 phút gắn tool
- Mở /vi/tools/dedupe/
- Dán mỗi mục một dòng (không dán cả mảng JSON
[...]trừ khi bạn đã tách sẵn) - Bật trim / ignore case nếu nguồn bẩn
- Bấm Loại bỏ trùng lặp → sao chép kết quả
- Nếu cần đưa vào code:
const arr = text.trim().split(/\r?\n/)
Khi list đã nằm trong biến JavaScript của app — viết Set/Map trong code, đừng round-trip qua tool.
Checklist trước khi merge PR có “dedupe”
- Giá trị nguyên thủy → có dùng Set (hoặc tương đương O(n))?
- Object → unique theo key nghiệp vụ (id/slug), không nhầm Set(object)?
- Đã
trim/ normalize case nếu nguồn paste? - Tiếng Việt: có thống nhất Unicode NFC khi so khớp chuỗi?
- Đã log hoặc assert
before.lengthvsafter.length? - List nhạy cảm: xử lý local (tool trình duyệt hoặc script local), không paste lên web lạ
FAQ ngắn trong bài
Lodash _.uniq còn cần không?
Team đã có lodash thì _.uniq / _.uniqBy ổn. Chỉ để dedupe primitive thì Set đủ, tránh thêm dependency.
includes trong vòng lặp?
Cùng họ O(n²) với indexOf — tránh với list lớn.
Empty string có bị giữ không?
Có, "" là một giá trị hợp lệ. Muốn bỏ dòng trống: .filter(Boolean) sau khi split.
Liên kết liên quan
- Loại bỏ trùng lặp (dedupe) — text mỗi dòng một mục, chạy local
- Đếm ký tự — kiểm tra length / UTF khi copy social
- Case Converter — thống nhất naming trước khi so khớp key
- Danh sách công cụ