Đối tượng `Set`
Bài viết này giải thích về đối tượng Set.
Chúng tôi sẽ giải thích đối tượng Set bằng các ví dụ thực tế.
YouTube Video
Đối tượng Set
Set là một đối tượng có sẵn được sử dụng để xử lý các tập hợp giá trị duy nhất, không trùng lặp. Nó cho phép bạn loại bỏ trùng lặp và kiểm tra sự tồn tại đơn giản hơn so với mảng, và giúp các thao tác tập hợp như hợp (union) và giao (intersection) dễ dàng thực hiện hơn.
Cơ bản: Tạo và sử dụng Set
Trước tiên, hãy xem cách tạo một Set, thêm và xóa phần tử, kiểm tra sự tồn tại và lấy kích thước của nó.
Dưới đây là mẫu cơ bản tạo một Set mới và trình bày các phương thức add, has, delete, và size.
1// Create a Set and demonstrate add, has, delete, and size
2const s = new Set();
3
4s.add(1);
5s.add(2);
6s.add(2); // duplicate, ignored
7
8console.log(s.has(1)); // true
9console.log(s.has(3)); // false
10
11s.delete(2);
12console.log(s.size); // 1
13
14console.log([...s]); // [1]
- Như được thể hiện trong đoạn mã này,
Settự động loại bỏ các giá trị nguyên thủy trùng lặp, và bạn có thể lấy số lượng phần tử bằng cách sử dụngsize.
Các phương pháp lặp
Set có thể lặp được, do đó bạn có thể duyệt qua nó bằng cách sử dụng for...of hoặc forEach. Thứ tự là theo thứ tự thêm vào.
Dưới đây là các cách sử dụng tiêu biểu của for...of và forEach.
1// Iterate a Set with for...of and forEach
2const s = new Set(['a', 'b', 'c']);
3
4for (const v of s) {
5 console.log('for...of:', v);
6}
7
8s.forEach((value, sameValue, setRef) => {
9 // Note: second arg is same as first for Set API to match Map signature
10 console.log('forEach:', value);
11});- Chữ ký callback cho
forEachlàvalue, value, set(để tương thích với Map), nhưng trong thực tế, bạn thường chỉ cần sử dụng đối sốvalueđầu tiên.
Chuyển đổi giữa Array và Set (hữu ích để loại bỏ trùng lặp)
Ở đây chúng tôi giới thiệu một kỹ thuật đơn giản để loại bỏ các phần tử trùng lặp từ một mảng, và cách chuyển đổi một Set về lại mảng.
Dưới đây là ví dụ về việc loại bỏ trùng lặp khỏi một mảng thông qua Set.
1// Deduplicate an array using Set
2const arr = [1, 2, 2, 3, 3, 3];
3const deduped = [...new Set(arr)];
4console.log(deduped); // [1, 2, 3]
5
6// Convert a Set to an array using Array.from
7const s = new Set([4, 5, 6]);
8const arrFromSet = Array.from(s);
9console.log(arrFromSet); // [4, 5, 6]
- Cách làm này ngắn gọn, nhanh chóng nên thường được sử dụng để loại bỏ trùng lặp trong mảng. Đặc biệt hiệu quả với các giá trị nguyên thủy.
Đối tượng và xử lý tham chiếu
Đối tượng trong Set được so sánh theo tham chiếu, vì vậy các phiên bản khác nhau nhưng cùng nội dung vẫn bị coi là các phần tử riêng biệt.
Đoạn mã sau đây minh họa điều gì xảy ra khi bạn thêm các đối tượng vào Set.
1// Objects are compared by reference in a Set
2const obj1 = { x: 1 };
3const obj2 = { x: 1 };
4
5const s = new Set();
6s.add(obj1);
7s.add(obj2);
8
9console.log(s.size); // 2 (different references)
10console.log(s.has(obj1)); // true
11console.log(s.has({ x: 1 })); // false (different object)
- Việc phát hiện trùng lặp đối với đối tượng dựa trên nhận dạng tham chiếu, vì vậy nếu bạn muốn loại bỏ trùng lặp dựa vào nội dung đối tượng, bạn sẽ cần tuần tự hóa (serialize) hoặc xử lý theo cách khác.
Giá trị đặc biệt: Xử lý NaN và -0/+0
Set sử dụng quy tắc so sánh Same-value-zero để xác định sự bằng nhau của giá trị. Phương pháp so sánh này có các đặc điểm sau đối với số:.
NaNđược coi là bằng nhau vớiNaN.+0và-0không được phân biệt và được coi là cùng một giá trị.
Vì vậy, khi bạn thêm những giá trị này vào Set, sẽ xảy ra những hành vi sau:.
1// NaN and zero behavior in Set
2const s = new Set();
3
4s.add(NaN);
5s.add(NaN);
6console.log(s.size); // 1 (NaN considered the same)
7
8s.add(+0);
9s.add(-0);
10console.log(s.size); // still 2 (NaN + 0)
11console.log([...s]); // [NaN, 0] (order may vary but only one zero)
- Trong so sánh thông thường (
NaN === NaN), nó trả vềfalse, nhưng trongSet, tất cả giá trịNaNđược coi là 'cùng một giá trị'. +0và-0có thể phân biệt về mặt toán học, nhưng trongSet, chúng chỉ được coi là0.- Kết quả là, chỉ còn một
NaNvà một0tồn tại trongSet. - Quy tắc so sánh của
Settương tự nhưObject.is, nhưng không hoàn toàn giống nhau.Object.is(+0, -0)trả vềfalse, nhưng trongSet, chúng được coi là giống nhau. Hãy chú ý sự khác biệt này.
Tiện ích phổ biến: Các thao tác trên Set (Hợp, Giao, Hiệu)
Các thao tác trên tập hợp có thể được viết rõ ràng hơn nhờ sử dụng Set. Dưới đây là các ví dụ triển khai phổ biến.
Dưới đây là các ví dụ về hàm cho union, intersection và difference.
1// Set operations: union, intersection, difference
2function union(a, b) {
3 return new Set([...a, ...b]);
4}
5
6function intersection(a, b) {
7 return new Set([...a].filter(x => b.has(x)));
8}
9
10function difference(a, b) {
11 return new Set([...a].filter(x => !b.has(x)));
12}
13
14// Demo
15const A = new Set([1, 2, 3]);
16const B = new Set([3, 4, 5]);
17
18console.log('union', [...union(A, B)]); // [1,2,3,4,5]
19console.log('intersection', [...intersection(A, B)]); // [3]
20console.log('difference A\\B', [...difference(A, B)]); // [1,2]
- Các phép toán trên tập hợp có thể được viết đơn giản bằng cách kết hợp bộ lọc với
Setvà mảng. Khi xử lý tập dữ liệu lớn, hiệu suất O(1) củahasgiúp các thao tác nhanh hơn.
Ví dụ thực tế: Tìm sự khác biệt giữa các mảng (Phát hiện mục đã thêm hoặc đã loại bỏ)
Ví dụ sau đây minh họa cách sử dụng Set để tìm sự khác biệt giữa hai mảng (danh sách cũ và danh sách mới). Điều này giúp bạn xác định được phần tử nào đã được thêm và phần tử nào đã bị xoá.
1// Find added and removed items between two arrays
2function diffArrays(oldArr, newArr) {
3 const oldSet = new Set(oldArr);
4 const newSet = new Set(newArr);
5
6 const added = [...newSet].filter(x => !oldSet.has(x));
7 const removed = [...oldSet].filter(x => !newSet.has(x));
8
9 return { added, removed };
10}
11
12const oldList = [1, 2, 3];
13const newList = [2, 3, 4, 5];
14
15console.log(diffArrays(oldList, newList));
16// { added: [4,5], removed: [1] }
- Phương pháp này rất tiện lợi để phát hiện sự khác biệt trong danh sách ID, danh sách thẻ hoặc các trường hợp tương tự. Sử dụng đơn giản nhất với các giá trị nguyên thủy.
Sự khác biệt giữa WeakSet và Set (Quản lý bộ nhớ)
WeakSet tương tự như Set, nhưng nó sử dụng tham chiếu yếu, cho phép các phần tử của nó được thu gom bộ nhớ tự động (garbage collected). Dưới đây là các cách sử dụng cơ bản của WeakSet.
1// WeakSet basics (objects only, not iterable)
2const ws = new WeakSet();
3let obj = { id: 1 };
4ws.add(obj);
5
6console.log(ws.has(obj)); // true
7
8obj = null; // Now the object is eligible for GC; WeakSet won't prevent collection
WeakSet chỉ có thể chứa các đối tượng và không thể lặp qua. Dưới đây là các ví dụ về giới hạn của WeakSet—chỉ chứa đối tượng và không lặp qua được.
1// WeakSet basics (objects only, not iterable)
2const ws = new WeakSet();
3
4// --- Only objects can be added ---
5try {
6 ws.add(1); // number
7} catch (e) {
8 console.log("Error: WeakSet can only store objects. Adding a number is not allowed.");
9}
10
11try {
12 ws.add("text"); // string
13} catch (e) {
14 console.log("Error: WeakSet can only store objects. Adding a string is not allowed.");
15}
16
17// --- WeakSet is not iterable ---
18try {
19 for (const value of ws) {
20 console.log(value);
21 }
22} catch (e) {
23 console.log("Error: WeakSet is not iterable. You cannot use for...of to loop over its elements.");
24}
25
26// --- Cannot convert to array ---
27try {
28 console.log([...ws]);
29} catch (e) {
30 console.log("Error: WeakSet cannot be converted to an array because it does not support iteration.");
31}
32
33// The object becomes eligible for garbage collection
34let obj = { id: 1 };
35ws.add(obj);
36obj = null;WeakSethữu ích để theo dõi tạm thời sự tồn tại của các đối tượng, nhưng bạn không thể liệt kê các phần tử hoặc lấy kích thước của nó.
Hiệu suất và lựa chọn khi sử dụng
Khi quyết định có nên sử dụng Set không, bạn cần hiểu đặc điểm về hiệu suất và bản chất dữ liệu của mình.
has,add, vàdeletethường hoạt động với hiệu suất gần như O(1) trung bình. Vì vậy, trong các trường hợp bạn thường xuyên kiểm tra sự tồn tại hoặc loại bỏ trùng lặp,Setthường ưu việt hơn mảng.- Hãy cẩn trọng nếu bạn muốn loại bỏ trùng lặp các đối tượng dựa vào nội dung (giá trị) của chúng. Vì
Setso sánh theo tham chiếu, một giải pháp thực tế là sử dụng ID hoặc khoá khác, hoặc tuần tự hoá đối tượng thành giá trị nguyên thủy trước khi sử dụng Set khi cần so sánh theo giá trị. Setđặc biệt hữu ích giúp tăng khả năng đọc hiểu cho mã khi xử lý tập hợp nhỏ đến vừa. Ngược lại, nếu bạn đang xử lý một số lượng lớn phần tử hoặc chuyển đổi thường xuyên giữa các mảng và Set, thì nên thực hiện kiểm thử và đánh giá hiệu suất thực tế.
Lỗi phổ biến
Set rất tiện lợi, nhưng nếu bạn không nắm rõ các quy tắc của nó, bạn có thể gặp phải những hành vi bất ngờ. Dưới đây là một số điểm thường gặp cần chú ý:.
- Đối tượng được so sánh theo tham chiếu, vì vậy ngay cả khi nội dung giống nhau, các đối tượng khác nhau cũng không bị coi là trùng lặp.
Setgiữ nguyên thứ tự thêm vào, nhưng không thể truy cập phần tử bằng chỉ số (index) như mảng. Nếu bạn muốn truy cập theo chỉ số, hãy chuyểnSetsang mảng trước.WeakSetkhông thể liệt kê phần tử, và chỉ lưu trữ đối tượng. Lưu ý rằng ứng dụng của nó bị giới hạn.NaNđược coi là cùng một giá trị, và+0và-0không được phân biệt. Điều này là do quy tắc so sánh Same-value-zero.
Tóm tắt
Set là một cấu trúc dữ liệu tiện lợi giúp bạn xử lý tập hợp các giá trị duy nhất một cách trực quan. Bạn có thể dùng nó để loại bỏ trùng lặp trong mảng, kiểm tra sự tồn tại nhanh chóng, hoặc thực hiện các phép toán hợp hoặc giao bằng mã nguồn ngắn gọn và dễ đọc.
Ngược lại, vì đối tượng được so sánh theo tham chiếu, nên cần có biện pháp bổ sung nếu bạn muốn so sánh dựa trên nội dung.
Bằng cách hiểu và sử dụng đúng các đặc điểm này, Set sẽ trở thành lựa chọn mạnh mẽ để nâng cao khả năng đọc hiểu và bảo trì mã nguồn.
Bạn có thể làm theo bài viết trên bằng cách sử dụng Visual Studio Code trên kênh YouTube của chúng tôi. Vui lòng ghé thăm kênh YouTube.