Xóa các mục trùng lặp trong mảng đối tượng JavaScript: Từ khóa đơn đến kết hợp nhiều khóa

1. Thách thức cốt lõi: Hiểu cách so sánh đối tượng trong JavaScript

Trong JavaScript, đối tượng (bao gồm cả mảng) là kiểu dữ liệu tham chiếu. Điều này có nghĩa là:

let doiTuong1 = { ma: 1, ten: "Alice" };
let doiTuong2 = { ma: 1, ten: "Alice" };
let doiTuong3 = doiTuong1;

console.log(doiTuong1 === doiTuong2); // false (nội dung giống nhau nhưng đây là hai địa chỉ bộ nhớ khác nhau/tham chiếu)
console.log(doiTuong1 === doiTuong3); // true (chỉ đến cùng một địa chỉ bộ nhớ/tham chiếu)

Vì vậy, bạn không thể sử dụng new Set(mangDoiTuong) để loại bỏ các mục trùng lặp, vì Set sẽ coi {ma:1, ten:"Alice"} và một {ma:1, ten:"Alice"} khác là các đối tượng khác nhau (khác tham chiếu).

Điều cần làm là xác định quy tắc cho “lặp lại”, thường dựa trên giá trị của một hoặc nhiều thuộc tính của đối tượng.

2. Phương pháp 1: Loại bỏ dựa trên khóa đơn (phổ biến và được khuyến nghị)

Đây là trường hợp phổ biến nhất, bạn muốn xác định xem một đối tượng có bị trùng lặp hay không dựa trên một thuộc tính cụ thể của nó (ví dụ: ma, email hoặc sku).

2.1 Sử dụng Map hoặc Set như bộ nhớ phụ trợ (được khuyến nghị)

Đây là phương pháp được khuyến nghị nhất, vì nó vừa hiệu quả vừa linh hoạt. Nó tận dụng tính duy nhất của khóa trong Map hoặc Set để theo dõi các giá trị thuộc tính đã được xử lý.

Nguyên lý:

  1. Tạo một Map hoặc Set để lưu trữ các định danh duy nhất (ví dụ: giá trị thuộc tính ma) đã gặp phải.
  2. Duyệt qua mảng đối tượng gốc.
  3. Đối với mỗi đối tượng, trích xuất giá trị khóa được sử dụng để xác định sự duy nhất.
  4. Kiểm tra xem Map hoặc Set đã chứa khóa này chưa:
    • Nếu chưa tồn tại, điều này cho thấy đây là một đối tượng duy nhất mới, thêm nó vào mảng kết quả và thêm khóa đó vào Map/Set.
    • Nếu đã tồn tại, điều này cho thấy đối tượng bị trùng lặp, bỏ qua.

Ví dụ (sử dụng Map, giữ lại đối tượng đầu tiên xuất hiện):

function loaiBoTrungLapBangKhoa(arr, khoa) {
    const daXem = new Map(); // Sử dụng Map lưu trữ các khóa đã xử lý và đối tượng tương ứng
    const ketQua = [];

    for (const phanTu of arr) {
        const dinhDan = phanTu[khoa]; // Lấy giá trị khóa dùng để xác định sự duy nhất
        if (!daXem.has(dinhDan)) { // Nếu Map chưa có khóa này
            daXem.set(dinhDan, phanTu); // Thêm khóa và đối tượng vào Map
            ketQua.push(phanTu);          // Thêm đối tượng vào mảng kết quả
        }
    }
    return ketQua;
}

// Dữ liệu mẫu
const nguoiDungs = [
    { ma: 1, ten: "Alice", tuoi: 30 },
    { ma: 2, ten: "Bob", tuoi: 25 },
    { ma: 1, ten: "Alicia", tuoi: 31 }, // ma trùng lặp
    { ma: 3, ten: "Charlie", tuoi: 35 },
    { ma: 2, ten: "Robert", tuoi: 26 }  // ma trùng lặp
];

const nguoiDungsDuyNhat = loaiBoTrungLapBangKhoa(nguoiDungs, 'ma');
console.log("Dựa trên 'ma' loại bỏ trùng lặp:", nguoiDungsDuyNhat);
/*
[
  { ma: 1, ten: 'Alice', tuoi: 30 },
  { ma: 2, ten: 'Bob', tuoi: 25 },
  { ma: 3, ten: 'Charlie', tuoi: 35 }
]
*/

const sanPhams = [
    { maSanPham: "A001", tieuDe: "Máy tính xách tay" },
    { maSanPham: "A002", tieuDe: "Chuột" },
    { maSanPham: "A001", tieuDe: "Máy tính chơi game" } // maSanPham trùng lặp
];
const sanPhamsDuyNhat = loaiBoTrungLapBangKhoa(sanPhams, 'maSanPham');
console.log("Dựa trên 'maSanPham' loại bỏ trùng lặp:", sanPhamsDuyNhat);
/*
[
  { maSanPham: 'A001', tieuDe: 'Máy tính xách tay' },
  { maSanPham: 'A002', tieuDe: 'Chuột' }
]
*/

Lợi ích:

  • Hiệu quả: Độ phức tạp thời gian gần bằng O(N) (vì các hoạt động hasset của Map là O(1) trung bình).
  • Linh hoạt: Có thể chỉ định bất kỳ thuộc tính nào làm định danh duy nhất.
  • Giữ nguyên thứ tự: Thứ tự các đối tượng trong mảng kết quả giữ nguyên thứ tự lần đầu tiên xuất hiện trong mảng gốc.
  • Xử lý khóa null/undefined: Map có thể sử dụng null hoặc undefined làm khóa, điều này hữu ích trong một số trường hợp.

Nhược điểm:

  • Một chút nhiều dòng hơn so với việc sử dụng trực tiếp Set.

2.2 Sử dụng filterfindIndex (lập trình hàm, nhưng hiệu suất thấp hơn)

Phương pháp này sử dụng Array.prototype.filter() kết hợp với Array.prototype.findIndex().

Nguyên lý:
Đối với mỗi phần tử trong mảng, hàm gọi lại của filter sẽ kiểm tra xem giá trị khóa duy nhất của phần tử đó có xuất hiện lần đầu tiên trong mảng gốc hay không. Nếu có, nó sẽ được giữ lại; ngược lại, nó sẽ bị lọc bỏ.

Ví dụ:

function loaiBoTrungLapBangLoc(arr, khoa) {
    return arr.filter((phanTu, chiSo, mang) => {
        // Tìm chỉ số của giá trị khóa của phần tử hiện tại trong phần tử trước chỉ số hiện tại
        // Nếu findIndex tìm thấy chỉ số giống với chỉ số hiện tại, điều này cho thấy là lần xuất hiện đầu tiên
        return chiSo === mang.findIndex(t => t[khoa] === phanTu[khoa]);
    });
}

const nguoiDungs = [
    { ma: 1, ten: "Alice" },
    { ma: 2, ten: "Bob" },
    { ma: 1, ten: "Alicia" },
];

const nguoiDungsDuyNhat = loaiBoTrungLapBangLoc(nguoiDungs, 'ma');
console.log("Dựa trên 'ma' loại bỏ trùng lặp (filter):", nguoiDungsDuyNhat);
/*
[
  { ma: 1, ten: 'Alice' },
  { ma: 2, ten: 'Bob' }
]
*/

Lợi ích:

  • Mã ngắn gọn, phong cách hàm.
  • Giữ lại thứ tự của đối tượng đầu tiên xuất hiện.

Nhược điểm:

  • Hiệu suất thấp: findIndex sẽ tìm kiếm từ đầu mảng mỗi lần lặp, dẫn đến độ phức tạp thời gian xấu nhất là O(N^2), hiệu suất giảm đáng kể với các mảng lớn.
  • Không khuyến nghị sử dụng cho các mảng lớn.

3. Phương pháp 2: Loại bỏ dựa trên các khóa kết hợp

Som khi, một thuộc tính đơn lẻ không đủ để xác định sự duy nhất. Ví dụ, bạn có thể cần sự kết hợp giữa tenDemho để xác định một người dùng là duy nhất.

Nguyên lý:
Tương tự như loại bỏ dựa trên khóa đơn, nhưng cần tạo ra một khoá hợp (thường là chuỗi), kết hợp các giá trị thuộc tính.

Ví dụ (sử dụng Map, dựa trên sự kết hợp giữa tenDemho):

function loaiBoTrungLapBangNhieuKhoa(arr, khoas) {
    const daXem = new Map();
    const ketQua = [];

    for (const phanTu of arr) {
        // Tạo một khóa hợp (ví dụ: "Alice|Smith|30")
        const khoaHop = khoas.map(khoa => phanTu[khoa]).join('|'); // Sử dụng '|' làm dấu phân cách

        if (!daXem.has(khoaHop)) {
            daXem.set(khoaHop, phanTu);
            ketQua.push(phanTu);
        }
    }
    return ketQua;
}

const nhanViens = [
    { tenDem: "John", ho: "Doe", tuoi: 30 },
    { tenDem: "Jane", ho: "Smith", tuoi: 25 },
    { tenDem: "John", ho: "Doe", tuoi: 31 }, // tenDem và ho trùng lặp, nhưng tuoi khác
    { tenDem: "Peter", ho: "Jones", tuoi: 40 },
    { tenDem: "Jane", ho: "Smith", tuoi: 26 }  // tenDem và ho trùng lặp
];

// Dựa trên sự kết hợp giữa 'tenDem' và 'ho'
const nhanViensDuyNhat = loaiBoTrungLapBangNhieuKhoa(nhanViens, ['tenDem', 'ho']);
console.log("Dựa trên sự kết hợp giữa 'tenDem' và 'ho' loại bỏ trùng lặp:", nhanViensDuyNhat);
/*
[
  { tenDem: 'John', ho: 'Doe', tuoi: 30 },
  { tenDem: 'Jane', ho: 'Smith', tuoi: 25 },
  { tenDem: 'Peter', ho: 'Jones', tuoi: 40 }
]
*/

Lợi ích:

  • Linh hoạt cao: Có thể xác định sự duy nhất dựa trên bất kỳ số lượng thuộc tính nào.
  • Hiệu quả: Cũng gần bằng O(N) độ phức tạp thời gian.

Nhược điểm:

  • Tạo khóa hợp có thể cần xem xét loại giá trị (ví dụ, số và chuỗi nối). Nếu giá trị chứa dấu phân cách, có thể gây ra vấn đề, nhưng thường có thể giải quyết bằng hàm băm phức tạp hơn (nhưng đối với hầu hết các trường hợp join('|') đủ).

4. Phương pháp 3: Sử dụng JSON.stringify() (chỉ phù hợp với các đối tượng đơn giản, có giới hạn nghiêm trọng)

Phương pháp này thông qua việc chuyển đổi mỗi đối tượng thành chuỗi JSON, sau đó tận dụng tính duy nhất của Set để loại bỏ trùng lặp.

Nguyên lý:

  1. Duyệt qua mảng, chuyển đổi mỗi đối tượng thành chuỗi JSON.
  2. Lưu trữ các chuỗi JSON này vào một Set, Set sẽ tự động xử lý các chuỗi trùng lặp.
  3. Chuyển đổi lại các chuỗi JSON duy nhất trong Set thành đối tượng JavaScript.

Ví dụ:

function loaiBoTrungLapBangJSON(arr) {
    const chuoiDuyNhat = new Set();
    const ketQua = [];

    for (const phanTu of arr) {
        const chuoiJSON = JSON.stringify(phanTu);
        if (!chuoiDuyNhat.has(chuoiJSON)) {
            chuoiDuyNhat.add(chuoiJSON);
            ketQua.push(JSON.parse(chuoiJSON)); // Chuyển đổi trở lại đối tượng
        }
    }
    return ketQua;
}

const phanTu = [
    { a: 1, b: "x" },
    { b: "x", a: 1 }, // Thứ tự khóa khác nhau, nhưng nội dung giống nhau
    { a: 2, b: "y" },
    { a: 1, b: "x" }
];

const phanTuDuyNhat = loaiBoTrungLapBangJSON(phanTu);
console.log("Dựa trên JSON.stringify loại bỏ trùng lặp:", phanTuDuyNhat);
/*
[
  { a: 1, b: 'x' },
  { a: 2, b: 'y' }
]
*/

Lợi ích:

  • Mã tương đối ngắn gọn.
  • Không cần chỉ định tên khóa, tự động so sánh tất cả các thuộc tính.
  • Lưu ý: Ngay cả khi thứ tự khóa khác nhau, JSON.stringify thường cũng sẽ tạo ra cùng một thứ tự khóa (tùy thuộc vào triển khai của máy chủ JavaScript, ví dụ V8 sẽ sắp xếp theo thứ tự chèn hoặc thứ tự chữ cái, nhưng điều này không nên được phụ thuộc). Tuy nhiên, theo chuẩn, JSON.stringify không đảm bảo thứ tự thuộc tính của đối tượng. Trong thực tế, hầu hết các máy chủ JavaScript sẽ giữ nguyên thứ tự định nghĩa thuộc tính khi xử lý đối tượng ký hiệu. Đối với đối tượng được tạo từ cấu trúc không có thứ tự (như Map), thứ tự có thể không thể đoán trước. Điều quan trọng hơn, nếu thứ tự khóa thay đổi trong quá trình tuần tự hóa và tái xây dựng, có thể khiến sai lầm đánh giá chúng là các chuỗi khác nhau.

Nhược điểm (nhiều, dẫn đến không khuyến nghị sử dụng trong các tình huống chung):

  • Tốn kém về hiệu suất: Cả JSON.stringifyJSON.parse đều là các hoạt động tốn thời gian, ảnh hưởng đáng kể đến hiệu suất đối với các mảng lớn hoặc các đối tượng phức tạp.
  • Hạn chế về kiểu dữ liệu:
    • Bỏ qua undefined, hàm, Symbol giá trị: Các thuộc tính này sẽ bị hoàn toàn loại bỏ trong JSON.stringify.
    • Date đối tượng sẽ được chuyển đổi thành chuỗi ISO: Không thể giữ lại kiểu đối tượng Date gốc.
    • NaNInfinity sẽ được chuyển đổi thành null.
  • Không thể xử lý tham chiếu vòng lặp: Nếu đối tượng chứa tham chiếu vòng lặp, JSON.stringify sẽ ném ra TypeError.
  • Tùy thuộc vào thứ tự khóa: Mặc dù trong hầu hết các máy chủ JavaScript hiện đại, JSON.stringify sẽ giữ nguyên thứ tự định nghĩa thuộc tính, nhưng chuẩn JSON không đảm bảo thứ tự của các khóa đối tượng. Điều này có nghĩa là hai đối tượng nội dung hoàn toàn giống nhau, nếu thứ tự định nghĩa thuộc tính khác nhau, có thể tạo ra các chuỗi JSON khác nhau, dẫn đến đánh giá sai rằng chúng không trùng lặp.
  • Không thể tùy chỉnh quy tắc loại bỏ trùng lặp: Chỉ có thể dựa trên tất cả các thuộc tính có thể tuần tự hóa của đối tượng toàn bộ để loại bỏ trùng lặp.

5. Xét nghiệm về hiệu suất

  • Map / Set hỗ trợ loại bỏ trùng lặp (phương pháp 1 và 2):
    • Độ phức tạp thời gian: Trung bình O(N), nơi N là độ dài của mảng. Điều này là do các hoạt động has()set() của Map hoặc Set trung bình là thời gian hằng số.
    • Độ phức tạp không gian: O(N), cần không gian bổ sung để lưu trữ Map hoặc Set.
    • Khuyến nghị sử dụng trong hầu hết các trường hợp, đặc biệt là khi có khóa duy nhất hoặc khóa hợp rõ ràng.
  • filter + findIndex kết hợp:
    • Độ phức tạp thời gian: Tốt nhất O(N^2), vì findIndex sẽ lặp lại từ đầu mảng mỗi lần lặp.
    • Độ phức tạp không gian: O(N) (tùy thuộc vào kích thước của mảng kết quả).
    • Không khuyến nghị sử dụng cho các mảng lớn, chỉ phù hợp cho các mảng rất nhỏ hoặc các trường hợp không nhạy cảm với hiệu suất.
  • Phương pháp JSON.stringify:
    • Độ phức tạp thời gian: Lý thuyết là O(N * M), nơi N là độ dài của mảng, M là độ phức tạp tuần tự hóa/phân tích đơn lẻ của đối tượng (tùy thuộc vào độ sâu của đối tượng và số lượng thuộc tính). Trong thực tế, thường nhanh hơn O(N^2) nhưng chậm hơn Map/Set.
    • Độ phức tạp không gian: O(N * L), nơi L là độ dài trung bình của chuỗi JSON.
    • Không khuyến nghị sử dụng trong các trường hợp chung, chủ yếu do hạn chế chức năng (mất dữ liệu, tham chiếu vòng lặp, phí hiệu suất). Chỉ nên cân nhắc khi đảm bảo cấu trúc đối tượng cực kỳ đơn giản, không có kiểu dữ liệu đặc biệt và yêu cầu hiệu suất không cao.

6. Tổng kết và tốt nhất thực hành

Khi chọn phương pháp loại bỏ trùng lặp trong mảng đối tượng, hãy tuân theo các nguyên tắc sau:

  1. Xác định định nghĩa “duy nhất”: Đầu tiên xác định thuộc tính nào (hoặc sự kết hợp của các thuộc tính nào) được sử dụng để xác định một đối tượng là duy nhất trong mảng đối tượng của bạn.
  2. Ưu tiên sử dụng Map hoặc Set hỗ trợ:
    • Khi bạn có thể chỉ định một khóa duy nhất (ví dụ: ma), sử dụng hàm loaiBoTrungLapBangKhoa (sử dụng Map) là được khuyến nghị, hiệu quả và linh hoạt nhất.
    • Khi cần xác định sự duy nhất dựa trên sự kết hợp của nhiều thuộc tính, sử dụng hàm loaiBoTrungLapBangNhieuKhoa (cũng sử dụng Map và khóa hợp) là lựa chọn tốt nhất.
  3. Tránh sử dụng filter + findIndex: Chỉ trừ khi mảng rất nhỏ, hiệu suất O(N^2) của nó trên các tập dữ liệu lớn sẽ kém.
  4. Cẩn thận khi sử dụng JSON.stringify(): Hạn chế của nó (mất dữ liệu, tham chiếu vòng lặp, phí hiệu suất) khiến nó không phù hợp với các trường hợp chung. Chỉ nên xem xét khi chắc chắn cấu trúc đối tượng cực kỳ đơn giản, không có kiểu dữ liệu đặc biệt và yêu cầu hiệu suất không cao.

Bằng cách chọn phương pháp đúng, bạn có thể quản lý hiệu quả và chính xác các dữ liệu trùng lặp trong mảng đối tượng JavaScript.

Thẻ: JavaScript set map array object

Đăng vào ngày 17 tháng 6 lúc 02:56