فرض کنید میخواهید لیستی از کاربران آنلاین را نگه دارید. با Array مینویسید:
const onlineUsers = []; function addUser(userId) { if (!onlineUsers.includes(userId)) { onlineUsers.push(userId); } } addUser(101); addUser(102); addUser(101); // تکراری!
مشکل چیست؟
Performance: ()includes باید کل آرایه را بگردد → O(n)
تکرار: باید خودمان چک کنیم که آیتم تکراری نباشد
حذف: ()splice برای حذف یک آیتم خاص کند است
راه حل: Set - یک ساختار داده که فقط مقادیر یکتا نگه میدارد و عملیاتهایش O(1) است.
// ایجاد Set خالی const mySet = new Set(); // ایجاد با مقادیر اولیه const numbers = new Set([1, 2, 3, 3, 4]); // {1, 2, 3, 4} // اضافه کردن mySet.add(10); mySet.add(20); mySet.add(10); // تکراری، اضافه نمیشود console.log(mySet.size); // 2 // چک کردن وجود console.log(mySet.has(10)); // true // حذف mySet.delete(10); // پاک کردن همه mySet.clear();
مقادیر تکراری:
Array: مجاز است، میتوانید چندین بار یک مقدار را اضافه کنید
Set: غیرمجاز، هر مقدار فقط یک بار ذخیره میشود
دسترسی با Index:
Array: arr[0] برای دسترسی به اولین عنصر
Set: ندارد، باید با Iterator یا تبدیل به Array دسترسی پیدا کنید
جستجو (has یا includes):
Array: پیچیدگی )O(n) - باید کل آرایه را بگردد
Set: پیچیدگی O(1) - جستجوی فوری با Hash Table
حذف عنصر:
Array: پیچیدگی O(n) - باید پیدا کند و Index ها را جابجا کند
Set: پیچیدگی O(1) - حذف فوری
حفظ ترتیب:
هر دو ترتیب اضافه شدن (insertion order) را حفظ میکنند
// روش قدیمی (کند) function removeDuplicates(arr) { return arr.filter((item, index) => arr.indexOf(item) === index); } // روش حرفهای با Set function removeDuplicatesFast(arr) { return [...new Set(arr)]; } const numbers = [1, 2, 2, 3, 4, 4, 5]; console.log(removeDuplicatesFast(numbers)); // [1, 2, 3, 4, 5]
Performance:
Array method: O(n^2) Set method:O(n)
// اجتماع (Union) function union(setA, setB) { return new Set([...setA, ...setB]); } // اشتراک (Intersection) function intersection(setA, setB) { return new Set([...setA].filter(x => setB.has(x))); } // تفاضل (Difference) function difference(setA, setB) { return new Set([...setA].filter(x => !setB.has(x))); } // مثال const admins = new Set(['user1', 'user2', 'user3']); const moderators = new Set(['user2', 'user3', 'user4']); console.log(union(admins, moderators)); // Set {'user1', 'user2', 'user3', 'user4'} console.log(intersection(admins, moderators)); // Set {'user2', 'user3'} console.log(difference(admins, moderators)); // Set {'user1'}
class OnlineUsersTracker { constructor() { this.onlineUsers = new Set(); } userConnected(userId) { this.onlineUsers.add(userId); console.log(`User ${userId} is online. Total: ${this.onlineUsers.size}`); } userDisconnected(userId) { this.onlineUsers.delete(userId); console.log(`User ${userId} went offline. Total: ${this.onlineUsers.size}`); } isOnline(userId) { return this.onlineUsers.has(userId); // O(1) } getOnlineCount() { return this.onlineUsers.size; } } // استفاده در WebSocket const tracker = new OnlineUsersTracker(); tracker.userConnected(101); tracker.userConnected(102); tracker.userConnected(101); // تکراری، اثری ندارد console.log(tracker.isOnline(101)); // true
class EventEmitter { constructor() { this.listeners = new Map(); // event -> Set of callbacks } on(event, callback) { if (!this.listeners.has(event)) { this.listeners.set(event, new Set()); } this.listeners.get(event).add(callback); } off(event, callback) { if (this.listeners.has(event)) { this.listeners.get(event).delete(callback); } } emit(event, data) { if (this.listeners.has(event)) { this.listeners.get(event).forEach(callback => callback(data)); } } } // استفاده const emitter = new EventEmitter(); const handler = (data) => console.log('Received:', data); emitter.on('message', handler); emitter.on('message', handler); // تکراری، فقط یک بار اضافه میشود emitter.emit('message', 'Hello'); // فقط یک بار اجرا میشود
Set: هر نوع دادهای (number, string, object, …)
WeakSet: فقط Object (نمیتوان primitive اضافه کرد)
Garbage Collection:
Set: رفرنس قوی نگه میدارد، مانع آزادسازی حافظه میشود
WeakSet: رفرنس ضعیف، اجازه میدهد Garbage Collector حافظه را آزاد کند
قابلیت Iterate:
Set: بله، میتوان با for...of یا forEach پیمایش کرد
WeakSet: خیر، غیرقابل پیمایش است
متدهای موجود:
Set: add, has, delete, clear, size
WeakSet: فقط add, has, delete (بدون clear و size)
کاربرد اصلی:
Set: ذخیره دائمی دادههای یکتا
WeakSet: ذخیره موقت برای Metadata و Tracking
// ❌ مشکل: Memory Leak let user = { id: 1, name: 'Gholi' }; const processedUsers = new Set(); processedUsers.add(user); user = null; // میخواهیم user را آزاد کنیم // اما Set هنوز رفرنس دارد! // Garbage Collector نمیتواند حافظه را آزاد کند console.log(processedUsers.size); // 1 -------- // ✅ راه حل: WeakSet let user = { id: 1, name: 'Gholi' }; const processedUsers = new WeakSet(); processedUsers.add(user); user = null; // حالا Garbage Collector میتواند حافظه را آزاد کند // WeakSet به صورت خودکار آبجکت را حذف میکند
۳.۳ کاربرد واقعی: ردیابی DOM Elements
class DOMTracker { constructor() { this.trackedElements = new WeakSet(); } track(element) { if (!(element instanceof HTMLElement)) { throw new Error('Only DOM elements can be tracked'); } this.trackedElements.add(element); console.log('Element tracked'); } isTracked(element) { return this.trackedElements.has(element); } } // استفاده const tracker = new DOMTracker(); let button = document.createElement('button'); tracker.track(button); console.log(tracker.isTracked(button)); // true // وقتی button از DOM حذف شود button.remove(); button = null; // WeakSet به صورت خودکار حافظه را آزاد میکند // Memory Leak نداریم!
const privateData = new WeakSet(); class SecureUser { constructor(name) { this.name = name; privateData.add(this); // علامتگذاری به عنوان معتبر } static isValid(user) { return privateData.has(user); } } // استفاده const user1 = new SecureUser('Gholi'); const fakeUser = { name: 'Hacker' }; console.log(SecureUser.isValid(user1)); // true console.log(SecureUser.isValid(fakeUser)); // false
function deepClone(obj, cloned = new WeakSet()) { // جلوگیری از Infinite Loop if (cloned.has(obj)) { throw new Error('Circular reference detected'); } if (typeof obj !== 'object' || obj === null) { return obj; } cloned.add(obj); const clone = Array.isArray(obj) ? [] : {}; for (let key in obj) { clone[key] = deepClone(obj[key], cloned); } return clone; } // مثال با Circular Reference const user = { name: 'Gholi' }; user.self = user; // Circular! try { deepClone(user); } catch (e) { console.log(e.message); // "Circular reference detected" }
// Benchmark: جستجو در 1 میلیون آیتم const size = 1_000_000; // Array const arr = Array.from({ length: size }, (_, i) => i); console.time('Array includes'); arr.includes(999_999); console.timeEnd('Array includes'); // ~10ms // Set const set = new Set(arr); console.time('Set has'); set.has(999_999); console.timeEnd('Set has'); // ~0.001ms (1000x سریعتر!)
// Array: 8 bytes per item (number) + overhead const arr = new Array(1_000_000).fill(1); // Set: ~24 bytes per item (hash table overhead) const set = new Set(arr); // نتیجه: Set حافظه بیشتری مصرف میکند // اما سرعت بالاتری دارد
قانون کلی:
اگر دادهها کمتر از ۱۰۰۰ آیتم: Array کافی است
اگر نیاز به جستجوی مکرر: Set
اگر نیاز به Index: Array
اگر نیاز به Garbage Collection: WeakSet
// ❌ کند: اضافه کردن تک تک const set = new Set(); for (let i = 0; i < 10000; i++) { set.add(i); } // ✅ سریع: ایجاد با آرایه const fastSet = new Set(Array.from({ length: 10000 }, (_, i) => i));
// ❌ خطرناک const userRoles = new Set(); userRoles.add('admin'); // حمله Object.prototype.isAdmin = true; console.log(userRoles.isAdmin); // true (خطرناک!) // ✅ امن function hasRole(set, role) { return set.has(role); // فقط از متدهای Set استفاده کن }
class SecureSet { constructor() { this.data = new Set(); } add(value) { // فقط مقادیر معتبر if (typeof value === 'string' && value.length < 100) { this.data.add(value); } else { throw new Error('Invalid input'); } } has(value) { return this.data.has(value); } }
class RateLimiter { constructor(maxRequests, windowMs) { this.maxRequests = maxRequests; this.windowMs = windowMs; this.requests = new Map(); // IP -> Set of timestamps } isAllowed(ip) { const now = Date.now(); if (!this.requests.has(ip)) { this.requests.set(ip, new Set()); } const userRequests = this.requests.get(ip); // حذف timestamp های قدیمی for (let timestamp of userRequests) { if (now - timestamp > this.windowMs) { userRequests.delete(timestamp); } } if (userRequests.size >= this.maxRequests) { return false; // Rate limit exceeded } userRequests.add(now); return true; } } // استفاده const limiter = new RateLimiter(5, 60000); // 5 requests per minute console.log(limiter.isAllowed('192.168.1.1')); // true // ... 5 بار دیگر console.log(limiter.isAllowed('192.168.1.1')); // false (blocked)
پاسخ:
Set فقط مقادیر یکتا نگه میدارد
عملیات has, add, delete در Set: O(1)O(1)O(1)، در Array: O(n)O(n)O(n)
Set Index ندارد، Array دارد
Set برای جستجوی سریع، Array برای ترتیب و دسترسی Index
پاسخ:
وقتی میخواهیم Object ها را Track کنیم بدون اینکه مانع Garbage Collection شویم
مثال: ردیابی DOM Elements، Private Data، جلوگیری از Circular Reference
پاسخ:
چون Garbage Collector میتواند هر لحظه آبجکتها را حذف کند. اگر Iterate میشد، نتیجه غیرقابل پیشبینی بود.
const set = new Set([1, 2, 3]); set.add(1); set.add('1'); console.log(set.size);
پاسخ: 4
چون 1 (number) و '1' (string) دو مقدار متفاوت هستند (strict equality).
const set = new Set([1, 2, 3]); // روش 1: Spread const arr1 = [...set]; // روش 2: Array.from const arr2 = Array.from(set); // روش 3: Loop const arr3 = []; set.forEach(item => arr3.push(item));
class MySet { constructor() { this.items = {}; this.length = 0; } add(value) { if (!this.has(value)) { this.items[value] = true; this.length++; } return this; } has(value) { return this.items.hasOwnProperty(value); } delete(value) { if (this.has(value)) { delete this.items[value]; this.length--; return true; } return false; } clear() { this.items = {}; this.length = 0; } get size() { return this.length; } }
// 1. برای یکتاسازی const uniqueIds = new Set(arrayWithDuplicates); // 2. برای جستجوی سریع const allowedUsers = new Set(['user1', 'user2', 'user3']); if (allowedUsers.has(currentUser)) { /* ... */ } // 3. برای عملیات مجموعهای const admins = new Set([...]); const moderators = new Set([...]); const allStaff = new Set([...admins, ...moderators]); // 4. WeakSet برای DOM tracking const clickedElements = new WeakSet(); element.addEventListener('click', () => { clickedElements.add(element); });
// 1. استفاده از Set برای دادههای کم const smallList = new Set([1, 2, 3]); // Array بهتر است // 2. نیاز به Index const set = new Set([1, 2, 3]); // set[0] ❌ کار نمیکند // 3. WeakSet با Primitive const weak = new WeakSet(); weak.add(123); // ❌ TypeError // 4. Iterate روی WeakSet const weak = new WeakSet(); for (let item of weak) { } // ❌ TypeError
برای مقادیر یکتا
Performance بالا O(1)
قابل Iterate
مصرف حافظه بیشتر از Array
فقط Object
Garbage Collection دوستانه
غیرقابل Iterate
برای Metadata و Tracking
const unique = [...new Set(array)];
انتخاب: Set
جستجوی مکرر (بیش از ۱۰۰۰ بار):
if (mySet.has(value)) { }
انتخاب: Set
ترتیب مهم است + نیاز به Index:
const item = arr[5];
انتخاب: Array
Track کردن Objects بدون Memory Leak:
const processed = new WeakSet(); processed.add(obj);
انتخاب: WeakSet
DOM Element tracking:
const clicked = new WeakSet(); element.addEventListener('click', () => clicked.add(element));
انتخاب: WeakSet
دادههای کمتر از ۱۰۰۰ آیتم:
const smallList = [1, 2, 3];
انتخاب: Array (سادگی مهمتر از Performance)
Rate Limiting:
const requests = new Map(); // IP -> Set of timestamps