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ý:
- Tạo một
MaphoặcSetđể lưu trữ các định danh duy nhất (ví dụ: giá trị thuộc tínhma) đã gặp phải. - Duyệt qua mảng đối tượng gốc.
- Đố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.
- Kiểm tra xem
MaphoặcSetđã 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.
- 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
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
hasvàsetcủaMaplà 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:Mapcó thể sử dụngnullhoặcundefinedlà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 filter và findIndex (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:
findIndexsẽ 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 tenDem và ho để 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 tenDem và ho):
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ý:
- Duyệt qua mảng, chuyển đổi mỗi đối tượng thành chuỗi JSON.
- Lưu trữ các chuỗi JSON này vào một
Set,Setsẽ tự động xử lý các chuỗi trùng lặp. - Chuyển đổi lại các chuỗi JSON duy nhất trong
Setthà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.stringifythườ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.stringifykhô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.stringifyvàJSON.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,Symbolgiá trị: Các thuộc tính này sẽ bị hoàn toàn loại bỏ trongJSON.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ượngDategốc.NaNvàInfinitysẽ được chuyển đổi thànhnull.
- Bỏ qua
- 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.stringifysẽ ném raTypeError. - 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.stringifysẽ 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/Sethỗ 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()vàset()củaMaphoặcSettrung 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ữ
MaphoặcSet. - 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.
- Độ 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
filter+findIndexkết hợp:- Độ phức tạp thời gian: Tốt nhất O(N^2), vì
findIndexsẽ 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ức tạp thời gian: Tốt nhất O(N^2), vì
- 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.
- Độ 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
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:
- 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.
- Ưu tiên sử dụng
MaphoặcSethỗ trợ:- Khi bạn có thể chỉ định một khóa duy nhất (ví dụ:
ma), sử dụng hàmloaiBoTrungLapBangKhoa(sử dụngMap) 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ụngMapvà khóa hợp) là lựa chọn tốt nhất.
- Khi bạn có thể chỉ định một khóa duy nhất (ví dụ:
- 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. - 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.