المجموعات والخرائط
مقدمة إلى المجموعات والخرائط
قدم ES6 نوعين قويين من المجموعات: Set (المجموعة) وMap (الخريطة). تملأ هياكل البيانات هذه فجوات مهمة لا تستطيع الكائنات والمصفوفات العادية معالجتها بكفاءة. المجموعة هي تجميع لقيم فريدة حيث لا يسمح بالتكرارات. والخريطة هي تجميع لأزواج مفتاح-قيمة حيث يمكن أن تكون المفاتيح من أي نوع وليس فقط سلاسل نصية. كلاهما يوفر خصائص أداء متفوقة لحالات الاستخدام المقصودة مقارنة بالمصفوفات والكائنات خاصة لعمليات مثل اختبار العضوية وإزالة التكرارات والبحث بالمفاتيح غير النصية.
قبل المجموعات والخرائط كان على المطورين استخدام المصفوفات مع indexOf() لفحوصات الفرادة والكائنات العادية لتخزين مفتاح-قيمة. كان لهذه الحلول البديلة قيود كبيرة: المصفوفات تتطلب وقت O(n) لفحوصات العضوية ومفاتيح الكائنات يتم تحويلها دائما إلى سلاسل نصية مما يعني أن obj[1] وobj["1"] يشيران إلى نفس الخاصية. تحل المجموعات والخرائط كلتا المشكلتين بتطبيقات مخصصة ومحسنة.
Set: الإنشاء والعمليات الأساسية
المجموعة تخزن قيما فريدة من أي نوع. يمكنك إنشاء مجموعة من عنصر قابل للتكرار (مثل مصفوفة) أو بناء واحدة تدريجيا باستخدام طريقة add(). تتعامل المجموعة تلقائيا مع الفرادة -- إذا حاولت إضافة قيمة موجودة بالفعل تتجاهل المجموعة ببساطة التكرار.
مثال: إنشاء المجموعات
// إنشاء مجموعة فارغة
const emptySet = new Set();
// إنشاء مجموعة من مصفوفة
const numbers = new Set([1, 2, 3, 4, 5]);
console.log(numbers); // Set(5) {1, 2, 3, 4, 5}
// التكرارات تزال تلقائيا
const withDupes = new Set([1, 2, 2, 3, 3, 3, 4]);
console.log(withDupes); // Set(4) {1, 2, 3, 4}
console.log(withDupes.size); // 4
// الإنشاء من سلسلة نصية (كل حرف يصبح عنصرا)
const chars = new Set('hello');
console.log(chars); // Set(4) {'h', 'e', 'l', 'o'}
// الإنشاء من أي عنصر قابل للتكرار
function* fibonacci() {
let a = 0, b = 1;
while (a < 20) {
yield a;
[a, b] = [b, a + b];
}
}
const fibSet = new Set(fibonacci());
console.log(fibSet); // Set(8) {0, 1, 2, 3, 5, 8, 13} -- التكرار 1 أزيل
add() و delete() و has() و size و clear()
توفر المجموعات واجهة برمجية مباشرة لإدارة محتوياتها. طريقة add() تعيد المجموعة نفسها مما يمكن من تسلسل الطرق. طريقة has() تنفذ البحث في وقت O(1) المتوسط وهو أسرع بشكل كبير من طريقة includes() في المصفوفات للمجموعات الكبيرة.
مثال: الطرق الأساسية للمجموعة
const tags = new Set();
// add() تعيد المجموعة لذا يمكنك التسلسل
tags.add('javascript')
.add('typescript')
.add('react')
.add('nodejs')
.add('javascript'); // تكرار -- يتم تجاهله بصمت
console.log(tags.size); // 4
// has() تفحص العضوية في وقت O(1)
console.log(tags.has('react')); // true
console.log(tags.has('angular')); // false
// delete() تزيل قيمة وتعيد true/false
console.log(tags.delete('react')); // true (تم إيجادها وإزالتها)
console.log(tags.delete('react')); // false (لم يتم إيجادها)
console.log(tags.size); // 3
// clear() تزيل جميع العناصر
tags.clear();
console.log(tags.size); // 0
// المجموعات تستخدم خوارزمية SameValueZero للمساواة
const mixedSet = new Set();
mixedSet.add(0);
mixedSet.add(-0); // 0 و -0 تعتبران متساويتين
mixedSet.add(NaN);
mixedSet.add(NaN); // NaN تعتبر مساوية لنفسها في المجموعات
console.log(mixedSet.size); // 2 (فقط 0 و NaN)
console.log(mixedSet.has(NaN)); // true -- بخلاف === حيث NaN !== NaN
===) لكن مع فرق واحد مهم: NaN تعتبر مساوية لـ NaN. هذا يعني أنه يمكنك تخزين والتحقق من NaN بشكل موثوق في المجموعة بخلاف طرق المصفوفات التي تستخدم ===. أيضا 0 و-0 تعاملان كنفس القيمة.التكرار على المجموعات
تحافظ المجموعات على ترتيب الإدراج مما يعني أن العناصر تتكرر بالترتيب الذي أضيفت به أولا. المجموعات قابلة للتكرار افتراضيا وتدعم عدة طرق تكرار.
مثال: التكرار على المجموعات
const frameworks = new Set(['React', 'Vue', 'Angular', 'Svelte']);
// حلقة for...of (الأكثر شيوعا)
for (const framework of frameworks) {
console.log(framework);
}
// "React", "Vue", "Angular", "Svelte"
// طريقة forEach
frameworks.forEach((value, valueAgain, set) => {
console.log(`${value} (${valueAgain})`);
// ملاحظة: value و valueAgain هما نفس الشيء!
// هذا التوقيع يطابق Map.forEach للتناسق
});
// مكرر values()
const valuesIter = frameworks.values();
console.log(valuesIter.next()); // { value: 'React', done: false }
console.log(valuesIter.next()); // { value: 'Vue', done: false }
// keys() هو اسم بديل لـ values() في المجموعات
// كلاهما يعيدان نفس المكرر
// entries() تعيد أزواج [value, value] (للتوافق مع Map)
for (const entry of frameworks.entries()) {
console.log(entry); // ['React', 'React'], ['Vue', 'Vue'], إلخ.
}
// عامل النشر يعمل لأن المجموعات قابلة للتكرار
const frameworkArray = [...frameworks];
console.log(frameworkArray); // ['React', 'Vue', 'Angular', 'Svelte']
// التفكيك يعمل أيضا
const [first, second, ...rest] = frameworks;
console.log(first); // "React"
console.log(second); // "Vue"
console.log(rest); // ["Angular", "Svelte"]
عمليات المجموعات: الاتحاد والتقاطع والفرق والفرق المتناظر
بينما لا تملك مجموعات JavaScript طرقا مدمجة لعمليات المجموعات مثل الاتحاد والتقاطع (رغم أن المقترحات قيد التنفيذ) فإن تطبيقها مباشر. هذه العمليات أساسية في الرياضيات ومفيدة للغاية في البرمجة لدمج ومقارنة وتصفية مجموعات البيانات.
مثال: عمليات المجموعات
const setA = new Set([1, 2, 3, 4, 5]);
const setB = new Set([4, 5, 6, 7, 8]);
// الاتحاد: جميع العناصر من كلتا المجموعتين
function union(a, b) {
return new Set([...a, ...b]);
}
console.log(union(setA, setB));
// Set(8) {1, 2, 3, 4, 5, 6, 7, 8}
// التقاطع: العناصر المشتركة في كلتا المجموعتين
function intersection(a, b) {
return new Set([...a].filter(x => b.has(x)));
}
console.log(intersection(setA, setB));
// Set(2) {4, 5}
// الفرق: العناصر في A وليست في B
function difference(a, b) {
return new Set([...a].filter(x => !b.has(x)));
}
console.log(difference(setA, setB));
// Set(3) {1, 2, 3}
// الفرق المتناظر: العناصر في أي مجموعة لكن ليس في كليهما
function symmetricDifference(a, b) {
const diff = new Set(a);
for (const elem of b) {
if (diff.has(elem)) {
diff.delete(elem);
} else {
diff.add(elem);
}
}
return diff;
}
console.log(symmetricDifference(setA, setB));
// Set(6) {1, 2, 3, 6, 7, 8}
// فحص المجموعة الجزئية: هل A مجموعة جزئية من B؟
function isSubset(a, b) {
return [...a].every(x => b.has(x));
}
console.log(isSubset(new Set([4, 5]), setB)); // true
console.log(isSubset(new Set([1, 4, 5]), setB)); // false
// فحص المجموعة الشاملة: هل A تحتوي جميع عناصر B؟
function isSuperset(a, b) {
return [...b].every(x => a.has(x));
}
console.log(isSuperset(setA, new Set([1, 3]))); // true
.union() و.intersection() و.difference() و.symmetricDifference() و.isSubsetOf() و.isSupersetOf() و.isDisjointFrom() يتم إضافتها إلى اللغة. تحقق من توافق المتصفحات قبل استخدامها لكنها متوفرة في الإصدارات الحديثة من المتصفحات الرئيسية و Node.js. هذه الطرق الأصلية أكثر كفاءة من التطبيقات اليدوية المعروضة أعلاه.التحويل بين المجموعة والمصفوفة
يمكن تحويل المجموعات والمصفوفات بسهولة ذهابا وإيابا. هذه القابلية للتبادل هي أحد أكثر الجوانب العملية للمجموعات حيث تسمح لك باستخدام المجموعة لإزالة التكرارات ثم التحويل مرة أخرى إلى مصفوفة لمزيد من المعالجة مع طرق المصفوفة مثل map() وfilter() وreduce().
مثال: تحويلات المجموعة-المصفوفة
// مصفوفة إلى مجموعة
const arr = [1, 2, 3, 2, 1, 4, 5, 4];
const uniqueSet = new Set(arr);
// مجموعة إلى مصفوفة -- ثلاث طرق
const way1 = [...uniqueSet]; // عامل النشر (الأكثر شيوعا)
const way2 = Array.from(uniqueSet); // Array.from()
const way3 = Array.from(uniqueSet.values()); // من المكرر
console.log(way1); // [1, 2, 3, 4, 5]
console.log(way2); // [1, 2, 3, 4, 5]
console.log(way3); // [1, 2, 3, 4, 5]
// إزالة التكرارات في سطر واحد
const unique = [...new Set([5, 3, 5, 2, 1, 3, 2, 4])];
console.log(unique); // [5, 3, 2, 1, 4] -- يحافظ على ترتيب الظهور الأول
// إزالة التكرارات مع التحويل
const words = ['Hello', 'HELLO', 'hello', 'World', 'world'];
const uniqueWords = [...new Set(words.map(w => w.toLowerCase()))];
console.log(uniqueWords); // ['hello', 'world']
أنماط إزالة التكرارات مع المجموعة
أحد أكثر الاستخدامات الشائعة والعملية للمجموعة هو إزالة التكرارات من المصفوفات. يظهر هذا النمط باستمرار في التطبيقات الحقيقية: إزالة نتائج API المكررة وضمان اختيارات المستخدم الفريدة وتصفية مستمعي الأحداث المتكررة والمزيد.
مثال: أنماط إزالة التكرارات الحقيقية
// إزالة تكرارات مصفوفة كائنات بخاصية محددة
function uniqueBy(array, keyFn) {
const seen = new Set();
return array.filter(item => {
const key = keyFn(item);
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
const users = [
{ id: 1, name: 'Alice', dept: 'Engineering' },
{ id: 2, name: 'Bob', dept: 'Marketing' },
{ id: 1, name: 'Alice', dept: 'Engineering' }, // مكرر
{ id: 3, name: 'Charlie', dept: 'Engineering' },
{ id: 2, name: 'Bob', dept: 'Marketing' } // مكرر
];
const uniqueUsers = uniqueBy(users, user => user.id);
console.log(uniqueUsers);
// [{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }, { id: 3, name: 'Charlie' }]
// إيجاد التكرارات في مصفوفة
function findDuplicates(array) {
const seen = new Set();
const duplicates = new Set();
for (const item of array) {
if (seen.has(item)) {
duplicates.add(item);
}
seen.add(item);
}
return [...duplicates];
}
console.log(findDuplicates([1, 2, 3, 2, 4, 5, 1])); // [2, 1]
// تتبع الزوار الفريدين
class UniqueVisitorTracker {
constructor() {
this.visitors = new Set();
}
recordVisit(userId) {
const isNew = !this.visitors.has(userId);
this.visitors.add(userId);
return isNew;
}
get uniqueCount() {
return this.visitors.size;
}
hasVisited(userId) {
return this.visitors.has(userId);
}
}
const tracker = new UniqueVisitorTracker();
console.log(tracker.recordVisit('user_1')); // true (زائر جديد)
console.log(tracker.recordVisit('user_2')); // true
console.log(tracker.recordVisit('user_1')); // false (زائر عائد)
console.log(tracker.uniqueCount); // 2
new Set([{a: 1}, {a: 1}]) سيكون حجمها 2 لأنها مرجعي كائنات مختلفين. إذا كنت بحاجة لإزالة تكرارات الكائنات حسب محتواها استخدم نمط uniqueBy() المعروض أعلاه مع دالة مفتاح.Map: الإنشاء والعمليات الأساسية
الخريطة هي مجموعة مرتبة من أزواج مفتاح-قيمة حيث يمكن أن تكون المفاتيح من أي نوع -- كائنات أو دوال أو أرقام أو حتى NaN. بخلاف الكائنات العادية حيث المفاتيح دائما سلاسل نصية أو رموز تحافظ الخرائط على النوع الأصلي للمفتاح. كما تحافظ الخرائط على ترتيب الإدراج وتوفر خاصية size للعد الفعال.
مثال: إنشاء الخرائط
// إنشاء خريطة فارغة
const emptyMap = new Map();
// إنشاء خريطة من مصفوفة أزواج [مفتاح، قيمة]
const userRoles = new Map([
['alice', 'admin'],
['bob', 'editor'],
['charlie', 'viewer']
]);
console.log(userRoles); // Map(3) {'alice' => 'admin', 'bob' => 'editor', 'charlie' => 'viewer'}
console.log(userRoles.size); // 3
// الإنشاء من Object.entries()
const config = { theme: 'dark', lang: 'en', fontSize: 16 };
const configMap = new Map(Object.entries(config));
console.log(configMap.get('theme')); // "dark"
// الخرائط يمكن أن تحتوي أي نوع كمفاتيح
const objectKeys = new Map();
const keyObj = { id: 1 };
const keyArr = [1, 2, 3];
const keyFn = () => 'hello';
objectKeys.set(keyObj, 'قيمة الكائن');
objectKeys.set(keyArr, 'قيمة المصفوفة');
objectKeys.set(keyFn, 'قيمة الدالة');
objectKeys.set(42, 'قيمة الرقم');
objectKeys.set(true, 'قيمة المنطقي');
objectKeys.set(null, 'قيمة Null');
objectKeys.set(undefined, 'قيمة Undefined');
console.log(objectKeys.get(keyObj)); // "قيمة الكائن"
console.log(objectKeys.get(42)); // "قيمة الرقم"
console.log(objectKeys.get(null)); // "قيمة Null"
console.log(objectKeys.size); // 7
set() و get() و has() و delete() و size و clear()
واجهة الخريطة البرمجية بديهية ومتسقة. مثل المجموعة طريقة set() تعيد الخريطة نفسها للتسلسل. جميع عمليات البحث بالمفتاح تعمل في وقت O(1) المتوسط مما يجعل الخرائط فعالة للغاية لتخزين مفتاح-قيمة.
مثال: الطرق الأساسية للخريطة
const inventory = new Map();
// set() تعيد الخريطة للتسلسل
inventory
.set('laptop', { price: 999, stock: 50 })
.set('phone', { price: 699, stock: 120 })
.set('tablet', { price: 449, stock: 75 })
.set('headphones', { price: 199, stock: 200 });
console.log(inventory.size); // 4
// get() تسترجع القيمة لمفتاح
const laptop = inventory.get('laptop');
console.log(laptop.price); // 999
console.log(laptop.stock); // 50
// get() تعيد undefined للمفاتيح المفقودة
console.log(inventory.get('desktop')); // undefined
// has() تفحص وجود مفتاح
console.log(inventory.has('phone')); // true
console.log(inventory.has('desktop')); // false
// تحديث قيمة (مجرد set مرة أخرى بنفس المفتاح)
inventory.set('laptop', { price: 899, stock: 45 });
console.log(inventory.get('laptop').price); // 899
// delete() تزيل زوج مفتاح-قيمة
console.log(inventory.delete('headphones')); // true
console.log(inventory.delete('headphones')); // false (أزيل بالفعل)
console.log(inventory.size); // 3
// clear() تزيل جميع المدخلات
const temp = new Map([['a', 1], ['b', 2]]);
temp.clear();
console.log(temp.size); // 0
التكرار على الخرائط
الخرائط قابلة للتكرار وتحافظ على ترتيب الإدراج. توفر ثلاث طرق مكرر: keys() وvalues() وentries(). المكرر الافتراضي للخرائط هو entries() مما يعني أن حلقة for...of تعطيك أزواج [مفتاح، قيمة] التي يمكنك تفكيكها مباشرة.
مثال: التكرار على الخرائط
const scores = new Map([
['Alice', 95],
['Bob', 87],
['Charlie', 92],
['Diana', 98]
]);
// for...of مع التفكيك (النمط الأكثر شيوعا)
for (const [name, score] of scores) {
console.log(`${name}: ${score}`);
}
// "Alice: 95", "Bob: 87", "Charlie: 92", "Diana: 98"
// forEach
scores.forEach((value, key) => {
console.log(`${key} حصل على ${value}`);
});
// مكرر keys()
for (const name of scores.keys()) {
console.log(name); // "Alice", "Bob", "Charlie", "Diana"
}
// مكرر values()
for (const score of scores.values()) {
console.log(score); // 95, 87, 92, 98
}
// مكرر entries() (مثل الافتراضي)
for (const [name, score] of scores.entries()) {
console.log(`${name}: ${score}`);
}
// تحويل الخريطة إلى مصفوفة مدخلات
const entriesArray = [...scores];
console.log(entriesArray);
// [['Alice', 95], ['Bob', 87], ['Charlie', 92], ['Diana', 98]]
// تحويل الخريطة إلى كائن (يعمل فقط مع المفاتيح النصية)
const scoresObj = Object.fromEntries(scores);
console.log(scoresObj);
// { Alice: 95, Bob: 87, Charlie: 92, Diana: 98 }
Map مقابل Object: متى تستخدم أيهما
الخرائط والكائنات العادية كلاهما يخزنان أزواج مفتاح-قيمة لكن لهما اختلافات مهمة تؤثر على أيهما يجب أن تختار. فهم هذه الاختلافات يساعدك على اتخاذ القرار المعماري الصحيح في كودك.
مثال: الفروقات الرئيسية بين Map و Object
// 1. أنواع المفاتيح
// الكائنات: المفاتيح دائما سلاسل نصية أو رموز
const obj = {};
obj[1] = 'مفتاح رقمي';
obj['1'] = 'مفتاح نصي';
console.log(Object.keys(obj)); // ["1"] -- مفتاح واحد فقط الرقم تحول
// الخرائط: المفاتيح تحتفظ بنوعها الأصلي
const map = new Map();
map.set(1, 'مفتاح رقمي');
map.set('1', 'مفتاح نصي');
console.log(map.size); // 2 -- مفتاحان منفصلان
console.log(map.get(1)); // "مفتاح رقمي"
console.log(map.get('1')); // "مفتاح نصي"
// 2. الحجم
// الكائنات: يجب الحساب يدويا
const objSize = Object.keys(obj).length; // O(n)
// الخرائط: .size هي خاصية -- O(1)
const mapSize = map.size;
// 3. ترتيب التكرار
// الكائنات: المفاتيح النصية بترتيب الإدراج ثم المفاتيح الرقمية مرتبة
const orderedObj = {};
orderedObj['b'] = 2;
orderedObj['a'] = 1;
orderedObj[2] = 'two';
orderedObj[1] = 'one';
console.log(Object.keys(orderedObj)); // ["1", "2", "b", "a"]
// الخرائط: دائما بترتيب الإدراج
const orderedMap = new Map();
orderedMap.set('b', 2);
orderedMap.set('a', 1);
orderedMap.set(2, 'two');
orderedMap.set(1, 'one');
console.log([...orderedMap.keys()]); // ['b', 'a', 2, 1]
// 4. تلوث النموذج الأولي
// الكائنات لها سلسلة نموذج أولي -- الخصائص الموروثة قد تسبب مشاكل
const plain = {};
console.log(plain['toString']); // [Function: toString] -- موروثة!
// الخرائط ليس لها مفاتيح موروثة
const safeMap = new Map();
console.log(safeMap.get('toString')); // undefined -- نظيفة
// 5. الأداء
// الخرائط محسنة للإضافات والحذف المتكرر
// الكائنات محسنة لبنى المفاتيح الثابتة مع تحسينات الشكل
Map مع مفاتيح غير نصية
واحدة من أقوى ميزات الخريطة هي قدرتها على استخدام أي قيمة كمفتاح. هذا يتيح أنماطا مستحيلة مع الكائنات العادية مثل استخدام عناصر DOM أو نسخ الأصناف أو الدوال كمفاتيح بحث.
مثال: الخرائط مع مفاتيح كائنات
// استخدام نسخ الأصناف كمفاتيح
class User {
constructor(name) {
this.name = name;
}
}
const permissions = new Map();
const alice = new User('Alice');
const bob = new User('Bob');
permissions.set(alice, ['read', 'write', 'admin']);
permissions.set(bob, ['read']);
console.log(permissions.get(alice)); // ['read', 'write', 'admin']
console.log(permissions.get(bob)); // ['read']
// استخدام الدوال كمفاتيح
const handlers = new Map();
function onClick() { console.log('تم النقر'); }
function onHover() { console.log('تم التمرير'); }
handlers.set(onClick, { count: 0, lastRun: null });
handlers.set(onHover, { count: 0, lastRun: null });
// زيادة عداد النقرات
const clickData = handlers.get(onClick);
clickData.count++;
console.log(handlers.get(onClick).count); // 1
// استخدام التعبيرات النمطية كمفاتيح
const validators = new Map();
validators.set(/^[\w.]+@[\w.]+\.\w+$/, 'تنسيق البريد الإلكتروني');
validators.set(/^\d{3}-\d{3}-\d{4}$/, 'تنسيق هاتف أمريكي');
validators.set(/^\d{5}(-\d{4})?$/, 'تنسيق رمز بريدي أمريكي');
// ملاحظة: مفاتيح التعبير النمطي تتطلب نفس المرجع للبحث
// يجب تخزين واستخدام نفس مرجع التعبير النمطي
تسلسل عمليات الخريطة
بما أن Map.set() تعيد الخريطة نفسها يمكنك تسلسل عدة استدعاءات set(). للتحويلات الأكثر تعقيدا غالبا ما تحول بين الخرائط والمصفوفات وتطبق طرق المصفوفة ثم تحول مرة أخرى.
مثال: تحويلات الخريطة والتسلسل
// تسلسل الطرق مع set()
const headers = new Map()
.set('Content-Type', 'application/json')
.set('Authorization', 'Bearer token123')
.set('Accept', 'application/json')
.set('Cache-Control', 'no-cache');
// تصفية خريطة (تحويل إلى مصفوفة وتصفية ثم تحويل مرة أخرى)
const prices = new Map([
['laptop', 999],
['mouse', 29],
['keyboard', 79],
['monitor', 449],
['cable', 12]
]);
// تصفية العناصر فوق 50 دولار
const expensive = new Map(
[...prices].filter(([, price]) => price > 50)
);
console.log(expensive);
// Map(3) {'laptop' => 999, 'keyboard' => 79, 'monitor' => 449}
// تعيين القيم (تطبيق خصم 10%)
const discounted = new Map(
[...prices].map(([item, price]) => [item, Math.round(price * 0.9)])
);
console.log(discounted);
// Map(5) {'laptop' => 899, 'mouse' => 26, 'keyboard' => 71, 'monitor' => 404, 'cable' => 11}
// ترتيب خريطة حسب القيمة
const sortedByPrice = new Map(
[...prices].sort((a, b) => a[1] - b[1])
);
console.log([...sortedByPrice.keys()]);
// ['cable', 'mouse', 'keyboard', 'monitor', 'laptop']
// تقليص خريطة إلى قيمة واحدة
const totalValue = [...prices.values()].reduce((sum, price) => sum + price, 0);
console.log(`إجمالي قيمة المخزون: $${totalValue}`); // "إجمالي قيمة المخزون: $1568"
أمثلة واقعية
المجموعات والخرائط لا تقدر بثمن في البرمجة اليومية. إليك عدة أنماط عملية توضح فائدتها في العالم الحقيقي.
إدارة جلسات المستخدم مع Map
مثال: إدارة الجلسات
class SessionManager {
constructor(maxAge = 30 * 60 * 1000) { // 30 دقيقة افتراضيا
this.sessions = new Map();
this.maxAge = maxAge;
}
createSession(userId, data = {}) {
const sessionId = crypto.randomUUID
? crypto.randomUUID()
: Math.random().toString(36).substring(2);
this.sessions.set(sessionId, {
userId,
data,
createdAt: Date.now(),
lastAccessed: Date.now()
});
return sessionId;
}
getSession(sessionId) {
const session = this.sessions.get(sessionId);
if (!session) return null;
// فحص انتهاء صلاحية الجلسة
if (Date.now() - session.lastAccessed > this.maxAge) {
this.sessions.delete(sessionId);
return null;
}
// تحديث وقت آخر وصول
session.lastAccessed = Date.now();
return session;
}
destroySession(sessionId) {
return this.sessions.delete(sessionId);
}
getActiveCount() {
this.cleanup();
return this.sessions.size;
}
cleanup() {
const now = Date.now();
for (const [id, session] of this.sessions) {
if (now - session.lastAccessed > this.maxAge) {
this.sessions.delete(id);
}
}
}
getUserSessions(userId) {
const userSessions = [];
for (const [id, session] of this.sessions) {
if (session.userId === userId) {
userSessions.push({ id, ...session });
}
}
return userSessions;
}
}
const sessions = new SessionManager(60000); // مهلة دقيقة واحدة
const sid = sessions.createSession('user_42', { role: 'admin' });
console.log(sessions.getSession(sid)); // { userId: 'user_42', data: {...}, ... }
console.log(sessions.getActiveCount()); // 1
التخزين المؤقت مع Map
مثال: تنفيذ ذاكرة التخزين المؤقت LRU
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.cache = new Map();
}
get(key) {
if (!this.cache.has(key)) return undefined;
// نقل إلى النهاية (الأحدث استخداما) بالحذف وإعادة الإضافة
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value);
return value;
}
put(key, value) {
// إذا كان المفتاح موجودا احذفه أولا (لتحديث الموضع)
if (this.cache.has(key)) {
this.cache.delete(key);
}
// إذا في السعة القصوى أزل المدخلة الأقدم (الأول في الخريطة)
if (this.cache.size >= this.capacity) {
const oldestKey = this.cache.keys().next().value;
this.cache.delete(oldestKey);
}
this.cache.set(key, value);
}
has(key) {
return this.cache.has(key);
}
get size() {
return this.cache.size;
}
clear() {
this.cache.clear();
}
}
const cache = new LRUCache(3);
cache.put('a', 1);
cache.put('b', 2);
cache.put('c', 3);
console.log(cache.get('a')); // 1 (ينقل 'a' إلى الأحدث)
cache.put('d', 4); // يطرد 'b' (الأقل استخداما مؤخرا)
console.log(cache.has('b')); // false (تم طرده)
console.log(cache.has('a')); // true
console.log(cache.has('c')); // true
console.log(cache.has('d')); // true
عد الوسوم وتحليل التكرار
مثال: العد باستخدام الخرائط
// عد تكرار الكلمات
function wordFrequency(text) {
const freq = new Map();
const words = text.toLowerCase().match(/\b\w+\b/g) || [];
for (const word of words) {
freq.set(word, (freq.get(word) || 0) + 1);
}
return freq;
}
const text = 'the quick brown fox jumps over the lazy dog the fox';
const freq = wordFrequency(text);
console.log(freq);
// Map(8) {'the' => 3, 'quick' => 1, 'brown' => 1, 'fox' => 2, ...}
// الحصول على أعلى N كلمات تكرارا
function topN(freqMap, n) {
return [...freqMap.entries()]
.sort((a, b) => b[1] - a[1])
.slice(0, n)
.map(([word, count]) => ({ word, count }));
}
console.log(topN(freq, 3));
// [{ word: 'the', count: 3 }, { word: 'fox', count: 2 }, { word: 'quick', count: 1 }]
// عد الوسوم لمنشورات المدونة
function countTags(posts) {
const tagCounts = new Map();
for (const post of posts) {
for (const tag of post.tags) {
tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
}
}
return tagCounts;
}
const blogPosts = [
{ title: 'أساسيات JS', tags: ['javascript', 'مبتدئ'] },
{ title: 'دليل React', tags: ['javascript', 'react', 'واجهة أمامية'] },
{ title: 'واجهة Node', tags: ['javascript', 'nodejs', 'خلفية'] },
{ title: 'شبكة CSS', tags: ['css', 'واجهة أمامية'] }
];
const tagStats = countTags(blogPosts);
console.log(tagStats);
// Map(7) {'javascript' => 3, 'مبتدئ' => 1, 'react' => 1,
// 'واجهة أمامية' => 2, 'nodejs' => 1, 'خلفية' => 1, 'css' => 1}
// تجميع المنشورات حسب الوسم
function groupByTag(posts) {
const groups = new Map();
for (const post of posts) {
for (const tag of post.tags) {
if (!groups.has(tag)) {
groups.set(tag, []);
}
groups.get(tag).push(post.title);
}
}
return groups;
}
const grouped = groupByTag(blogPosts);
console.log(grouped.get('javascript'));
// ['أساسيات JS', 'دليل React', 'واجهة Node']
console.log(grouped.get('واجهة أمامية'));
// ['دليل React', 'شبكة CSS']
WeakSet و WeakMap
يوفر JavaScript أيضا متغيرات WeakSet وWeakMap التي تحمل مراجع ضعيفة لمفاتيحها (أو قيمها في حالة WeakSet). هذا يعني أن المدخلات يتم جمعها تلقائيا عندما لم يعد كائن المفتاح مشار إليه في مكان آخر من برنامجك. المجموعات الضعيفة ليست قابلة للتكرار وليس لها خاصية size لكنها تمنع تسرب الذاكرة في السيناريوهات التي تربط فيها بيانات بكائنات قد تزال.
مثال: WeakMap للبيانات الخاصة
// WeakMap لبيانات النسخة الخاصة حقا
const _private = new WeakMap();
class Account {
constructor(owner, balance) {
_private.set(this, {
owner,
balance,
transactions: []
});
}
deposit(amount) {
const data = _private.get(this);
data.balance += amount;
data.transactions.push({ type: 'إيداع', amount, date: new Date() });
}
withdraw(amount) {
const data = _private.get(this);
if (amount > data.balance) {
throw new Error('رصيد غير كاف');
}
data.balance -= amount;
data.transactions.push({ type: 'سحب', amount, date: new Date() });
}
get balance() {
return _private.get(this).balance;
}
get owner() {
return _private.get(this).owner;
}
}
const acct = new Account('Alice', 1000);
acct.deposit(500);
acct.withdraw(200);
console.log(acct.balance); // 1300
console.log(acct.owner); // "Alice"
// البيانات الخاصة غير قابلة للوصول حقا من الخارج
console.log(Object.keys(acct)); // []
console.log(Object.getOwnPropertySymbols(acct)); // []
// لا طريقة للوصول إلى بيانات _private بدون مرجع WeakMap
// WeakSet لتتبع الكائنات المعالجة
const processed = new WeakSet();
function processOnce(obj) {
if (processed.has(obj)) {
console.log('تمت المعالجة بالفعل، يتم التخطي');
return;
}
processed.add(obj);
console.log('جاري المعالجة:', obj);
}
const data = { id: 1 };
processOnce(data); // "جاري المعالجة: { id: 1 }"
processOnce(data); // "تمت المعالجة بالفعل، يتم التخطي"
WeakMap عندما تحتاج لربط بيانات بكائنات قد يتم جمعها (مثل عناصر DOM أو نسخ الأصناف). استخدم WeakSet عندما تحتاج لتتبع ما إذا كان كائن قد شوهد أو عولج. كلمة "ضعيف" في WeakMap و WeakSet تعني أن المجموعة لا تمنع جامع القمامة من استعادة كائنات المفاتيح وهو أمر ضروري لمنع تسرب الذاكرة في التطبيقات طويلة التشغيل.دمج المجموعات والخرائط
تعمل المجموعات والخرائط معا بشكل رائع. إليك مثال عملي يجمع كلا هيكلي البيانات لحل مشكلة شائعة.
مثال: نظام صلاحيات مع المجموعات والخرائط
class PermissionSystem {
constructor() {
// خريطة من اسم الدور إلى مجموعة الصلاحيات
this.roles = new Map();
// خريطة من معرف المستخدم إلى مجموعة أسماء الأدوار
this.userRoles = new Map();
}
defineRole(roleName, permissions) {
this.roles.set(roleName, new Set(permissions));
}
assignRole(userId, roleName) {
if (!this.roles.has(roleName)) {
throw new Error(`الدور "${roleName}" غير موجود`);
}
if (!this.userRoles.has(userId)) {
this.userRoles.set(userId, new Set());
}
this.userRoles.get(userId).add(roleName);
}
getUserPermissions(userId) {
const roles = this.userRoles.get(userId);
if (!roles) return new Set();
const allPermissions = new Set();
for (const role of roles) {
const perms = this.roles.get(role);
if (perms) {
for (const perm of perms) {
allPermissions.add(perm);
}
}
}
return allPermissions;
}
hasPermission(userId, permission) {
return this.getUserPermissions(userId).has(permission);
}
}
const perms = new PermissionSystem();
// تعريف الأدوار مع مجموعات الصلاحيات
perms.defineRole('viewer', ['read']);
perms.defineRole('editor', ['read', 'write', 'comment']);
perms.defineRole('admin', ['read', 'write', 'comment', 'delete', 'manage-users']);
// تعيين الأدوار للمستخدمين
perms.assignRole('user_1', 'editor');
perms.assignRole('user_1', 'viewer'); // الصلاحيات المتداخلة تُزال تكراراتها
perms.assignRole('user_2', 'admin');
console.log([...perms.getUserPermissions('user_1')]);
// ['read', 'write', 'comment']
console.log(perms.hasPermission('user_1', 'write')); // true
console.log(perms.hasPermission('user_1', 'delete')); // false
console.log(perms.hasPermission('user_2', 'manage-users')); // true
ملخص المفاهيم الرئيسية
- Set تخزن قيما فريدة مع عمليات
has()وadd()وdelete()بوقت O(1). استخدمها لإزالة التكرارات وفحوصات العضوية السريعة. - عمليات المجموعات (الاتحاد والتقاطع والفرق والفرق المتناظر) يمكن تنفيذها بدمج النشر و
filter()أو باستخدام الطرق الأصلية الجديدة في البيئات الحديثة. - Map تخزن أزواج مفتاح-قيمة بأي نوع مفتاح وتحافظ على ترتيب الإدراج وتوفر بحث O(1) مع
get()وhas(). - Map مقابل Object: اختر Map عندما تكون المفاتيح ديناميكية أو غير نصية أو عندما تحتاج ترتيبا مضمونا وإضافات أو حذف متكرر.
- WeakSet و WeakMap تحمل مراجع ضعيفة تسمح بجمع القمامة مما يمنع تسرب الذاكرة عند ربط بيانات بالكائنات.
- الأنماط الواقعية تشمل إدارة الجلسات والتخزين المؤقت (LRU) وعد التكرار وأنظمة الصلاحيات وإزالة التكرارات.
تمرين عملي
ابنِ صفا TagManager كاملا يستخدم كلا من Set و Map داخليا. يجب أن يدعم الصف: (1) إضافة وسوم للعناصر باستخدام Map حيث يرتبط كل معرف عنصر بمجموعة Set من الوسوم. (2) طريقة getItemsByTag(tag) تعيد جميع معرفات العناصر التي لديها وسم محدد. (3) طريقة getRelatedTags(tag) تعيد مجموعة Set من جميع الوسوم التي تظهر على أي عنصر يحتوي أيضا الوسم المعطى (مع استبعاد الوسم المعطى نفسه). (4) طريقة getMostPopularTags(n) تستخدم خريطة تكرار لإعادة أعلى N وسم مرتبة حسب عدد العناصر التي تستخدمها. (5) طريقة mergeTags(oldTag, newTag) تستبدل جميع ظهور oldTag بـ newTag عبر جميع العناصر. اختبر تنفيذك مع 10 عناصر على الأقل و15 وسما مختلفا. تحقق من أن إزالة التكرارات تعمل (إضافة نفس الوسم مرتين لعنصر يخزنه مرة واحدة فقط) وأن عد الوسوم صحيح وأن getRelatedTags() تنتج نتائج دقيقة. أخيرا نفذ ذاكرة تخزين مؤقت LRU باستخدام Map تخزن نتائج getItemsByTag() وتبطل التخزين المؤقت كلما تم تعديل الوسوم.