Lập trình · 19/10/2026

Loại trùng dữ liệu trong JavaScript: chọn đúng khóa và quy tắc giữ bản ghi

Danh sách nhận về hai bản ghi cùng ID nhưng khác tên; thêm new Set vào code vẫn không loại được bản trùng. Trước khi chọn cú pháp, cần xác định “trùng” theo giá trị, tham chiếu object hay một khóa nghiệp vụ. Sau đó mới quyết định bản ghi nào được giữ và thứ tự đầu ra.

Loại trùng dữ liệu trong JavaScript: chọn đúng khóa và quy tắc giữ bản ghi

Danh sách nhận về hai bản ghi cùng ID nhưng khác tên; thêm new Set vào code vẫn không loại được bản trùng. Trước khi chọn cú pháp, cần xác định “trùng” theo giá trị, tham chiếu object hay một khóa nghiệp vụ. Sau đó mới quyết định bản ghi nào được giữ và thứ tự đầu ra.

Bài viết chỉ biến đổi dữ liệu trong bộ nhớ, không xóa bản ghi trong database. Loại trùng để hiển thị không tự xử lý nguyên nhân API trả dữ liệu lặp hoặc xung đột cập nhật ở nguồn.

1. Set phù hợp với giá trị nguyên thủy theo quy tắc của nó

const labels = ['api', 'web', 'api', 'API'];
const unique = [...new Set(labels)];

console.log(unique.join(',')); // api,web,API
console.log(new Set([1, '1']).size); // 2
console.log(new Set([NaN, NaN, 0, -0]).size); // 2

Thứ tự giữ lại là thứ tự thêm thành công vào Set. api và API khác nhau, số 1 khác chuỗi "1". Theo SameValueZero, NaN được coi trùng NaN, còn 0 và -0 thuộc cùng một giá trị trong tập.

Nếu sản phẩm cần không phân biệt hoa/thường hay chuẩn hóa Unicode, hãy định nghĩa khóa so sánh riêng. Đừng tự đổi mọi nhãn vì việc gộp có thể làm mất khác biệt người dùng muốn giữ.

2. Object có cùng nội dung chưa chắc là cùng phần tử

const a = { id: '7', name: 'Lan' };
const b = { id: '7', name: 'Lan' };

console.log(new Set([a, b]).size); // 2
console.log(new Set([a, a]).size); // 1

Set so object bằng danh tính tham chiếu, không so sâu nội dung. Hai object được tạo riêng vẫn là hai phần tử dù có cùng id và name. Với dữ liệu API, các bản ghi lặp thường là object riêng nên cần khóa rõ ràng.

3. Giữ bản ghi đầu theo ID chuỗi

Hàm minh họa yêu cầu mảng bản ghi thông thường với id là chuỗi không rỗng. Nó không trim hoặc chuyển ID số thành chuỗi, không sao chép sâu và không kiểm tra toàn bộ schema nghiệp vụ.

function firstById(rows) {
  if (!Array.isArray(rows)) {
    throw new TypeError('Expected a record array');
  }
  const seen = new Set();
  const result = [];
  for (const row of rows) {
    if (row === null || typeof row !== 'object' || Array.isArray(row)
        || typeof row.id !== 'string' || row.id.length === 0) {
      throw new TypeError('Expected a record with a nonempty string ID');
    }
    if (!seen.has(row.id)) {
      seen.add(row.id);
      result.push(row);
    }
  }
  return result;
}

const rows = [
  { id: 'a', name: 'Old' },
  { id: 'b', name: 'Other' },
  { id: 'a', name: 'New' }
];
console.log(firstById(rows).map(row => row.name).join(',')); // Old,Other

Thứ tự đầu ra đi theo lần xuất hiện đầu của mỗi ID. Trong chính sách này, bản Old thắng vì xuất hiện trước, không phải vì dữ liệu của nó đúng hơn. Nếu thứ tự API không được bảo đảm, “giữ đầu” cũng không tự tạo ra quy tắc chọn đáng tin về thời gian.

Hàm trả mảng mới nhưng các object vẫn tham chiếu tới nguồn. Sửa thuộc tính object trong kết quả có thể sửa dữ liệu đang được nơi khác dùng chung. Kiểm tra riêng nhu cầu bản sao độc lập nếu bên gọi tiếp tục chỉnh sửa.

4. Map giữ giá trị cuối nhưng vị trí khóa theo lần thêm đầu

const rows = [
  { id: 'a', name: 'Old' },
  { id: 'b', name: 'Other' },
  { id: 'a', name: 'New' }
];
const byId = new Map(rows.map(row => [row.id, row]));
const result = [...byId.values()];

console.log(result.map(row => row.name).join(',')); // New,Other
console.log(result.map(row => row.id).join(','));   // a,b

Ví dụ giả định rows đã qua validation. Giá trị New thay Old ở khóa a, nhưng khóa a không tự chuyển ra cuối khi cập nhật. Vì vậy “giữ bản ghi cuối” và “sắp theo vị trí xuất hiện cuối” là hai yêu cầu khác nhau.

Cuối trong mảng cũng không đồng nghĩa mới nhất theo nghiệp vụ. Nếu chọn theo version hay thời điểm cập nhật, cần so trường đó và quy định khi hòa, thiếu version hoặc timestamp không hợp lệ.

5. Khóa ghép cần giữ được ranh giới

const left = ['a:b', 'c'];
const right = ['a', 'b:c'];

console.log(left.join(':') === right.join(':')); // true
console.log(JSON.stringify(left) === JSON.stringify(right)); // false

Ghép tenantId và id bằng dấu hai chấm có thể gây va chạm nếu thành phần chứa dấu đó. Với tuple cố định gồm các chuỗi đã xác nhận, JSON.stringify cho một biểu diễn phân biệt ranh giới như ví dụ. Một lựa chọn khác là Map lồng nhau.

Đừng suy rộng thành dùng JSON.stringify toàn bộ object làm khóa so sâu tổng quát: kiểu dữ liệu, trường bị bỏ qua và thứ tự thuộc tính cần được xem xét. Hãy chọn những trường thật sự định danh bản ghi.

6. Checklist chính sách loại trùng

  • Ghi rõ khóa và kiểu dữ liệu của khóa.
  • Chọn giữ đầu, giữ cuối, chọn theo version hoặc báo xung đột.
  • Quy định thứ tự đầu ra độc lập với quy tắc giữ bản ghi.
  • Thử ID thiếu, sai kiểu, chuỗi rỗng và khóa ghép có dấu phân cách.
  • Kiểm tra tham chiếu object và mảng nguồn.
  • Theo dõi số bản trùng nếu việc bỏ qua có thể che lỗi nguồn.
  • Giữ ràng buộc dữ liệu ở server; loại trùng trong bộ nhớ không ngăn ghi trùng đồng thời.

Các đoạn code đã chạy bằng Node.js local; không thao tác database và không đo hiệu năng. Tài liệu đối chiếu: Set và Map.

Loại trùng tốt bắt đầu từ một quyết định dữ liệu rõ ràng. Một dòng code ngắn chỉ đúng khi khóa, bản ghi được giữ và thứ tự đã khớp với yêu cầu.

Thảo luận

Bình luận 0

Đăng nhập để bình luận

Bạn cần có tài khoản để tham gia thảo luận và trả lời độc giả khác.

Đăng nhậpĐăng ký

Chưa có bình luận. Hãy là người đầu tiên chia sẻ ý kiến.