Đố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, Set tự độ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ụng size.

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...offorEach.

 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 forEachvalue, 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-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ới NaN.
  • +0-0 khô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 trong Set, tất cả giá trị NaN được coi là 'cùng một giá trị'.
  • +0-0 có thể phân biệt về mặt toán học, nhưng trong Set, chúng chỉ được coi là 0.
  • Kết quả là, chỉ còn một NaN và một 0 tồn tại trong Set.
  • Quy tắc so sánh của Set tươ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 trong Set, 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, intersectiondifference.

 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 Set và mảng. Khi xử lý tập dữ liệu lớn, hiệu suất O(1) của has giú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;
  • WeakSet hữ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à delete thườ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, Set thườ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ì Set so 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.
  • Set giữ 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ển Set sang mảng trước.
  • WeakSet khô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à +0-0 khô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.

YouTube Video