مجموعتان تتجاوزان Object و Array
الكائنات العادية والمصفوفات تكفي لمعظم ما يحتاجه أي برنامج JavaScript، لكنها لم تُصمَّم لكل المهام. هنا يأتي دور Map و Set في JavaScript: مجموعتان مدمجتان في اللغة تسدّان ثغرتين محددتين، وهما البحث بمفاتيح ليست نصية، والتحقق السريع من وجود عنصر بدون تكرار.
هاتان البنيتان موجودتان في اللغة منذ إصدار ES2015. كلاهما قابل للتكرار (iterable)، ولكل منهما خاصية .size، ويتعاملان جيدًا مع عامل الانتشار (spread operator). الفكرة ببساطة:
Map— مثل الكائن، لكن المفاتيح يمكن أن تكون من أي نوع، وترتيب الإدخال محفوظ.Set— مثل المصفوفة، لكن القيم فريدة والبحث فيها سريع.
إنشاء Map واستخدامه
يحتفظ Map بأزواج من المفاتيح والقيم. تُنشئه عبر new Map()، وتتعامل معه من خلال .set() و .get() و .has() و .delete():
يمكنك أيضًا تمرير مصفوفة من أزواج [key, value] إلى المُنشئ (constructor) لتعبئتها بقيم ابتدائية:
شكل المصفوفة ثنائية العناصر هذا ستلاقيه في كل مكان مع الـ Map — لأنه ببساطة الطريقة اللي تتمثل بها المدخلات (entries) أثناء التكرار.
الفرق بين Map و Object: ليش نستخدم Map أصلاً؟
الكائنات العادية (Objects) تبدو وكأنها تؤدي نفس الغرض، وفي معظم الحالات فعلاً تؤديه. لكن Map يعالج بعض النقاط المزعجة تحديداً:
الكائنات (Objects) ترث من Object.prototype، ولذلك فإن مفاتيح مثل toString وconstructor وhasOwnProperty موجودة أصلاً في كل كائن. أما Map فلا تحمل هذا العبء — المفاتيح التي تضيفها بنفسك هي المفاتيح الوحيدة الموجودة فعلاً.
وهناك فروق أخرى تستحق الانتباه:
- أي نوع من المفاتيح. يقبل
Mapالكائنات والدوال والأرقام والقيم المنطقية كمفاتيح. أما الكائنات فتحوّل أي مفتاح غير نصي إلى نص بصمت:obj[1]وobj["1"]يشيران إلى نفس الخانة. - ترتيب إدراج مضمون. يمرّ
Mapعلى العناصر بنفس ترتيب إضافتها. الكائنات تفعل ذلك في الغالب، لكن المفاتيح النصية التي تبدو كأرقام تُرتَّب في المقدمة أولاً — وهذه مصيدة خفية. - حجم جاهز. الخاصية
map.sizeتعمل بزمن ثابت O(1)، بينما في الكائن تحتاج إلى كتابةObject.keys(obj).lengthالتي تبني مصفوفة جديدة في كل مرة. - مُحسَّن للتغيير المتكرر. محركات JavaScript تضبط
Mapلأداء عمليات الإضافة والحذف المتكررة، في حين تُضبط الكائنات للسجلات ذات الشكل الثابت.
استخدم الكائن حين تمثّل سجلاً بمفاتيح نصية معروفة مسبقاً مثل ({ name, email, age })، واستخدم Map حين تكون المفاتيح ديناميكية أو غير نصية، أو حين تكثر عمليات الإضافة والحذف.
التكرار على Map
كائنات Map قابلة للتكرار (iterable)، أي أن حلقة for...of تعمل عليها مباشرة، ويمكنك تفكيك كل إدخال (entry) بسهولة:
إذا كنت تحتاج المفاتيح فقط أو القيم فقط، استخدم .keys() أو .values(). وتتوفر أيضًا .forEach() إذا كنت تفضّلها:
لتحويل الـ Map إلى كائن عادي أو مصفوفة، استخدم عامل النشر (spread):
إنشاء Set واستخدامه في JavaScript
تخزّن بنية Set في جافا سكريبت قيمًا فريدة فقط، فإذا حاولت إضافة قيمة موجودة مسبقًا فلن يحدث شيء:
تحديد تفرد العناصر يعتمد على نفس قاعدة المساواة الصارمة ===، مع استثناء بسيط: قيمة NaN تُعتبر مساوية لنفسها داخل الـ Set، رغم أن NaN === NaN تُرجع false في أي مكان آخر.
يمكنك تمرير أي عنصر قابل للتكرار (iterable) إلى الـ constructor لتعبئة الـ Set مباشرة، ومن هنا تأتي حيلة إزالة التكرار من مصفوفة JavaScript:
سطر واحد، ويصلح مع أي نوع بدائي. أما مع مصفوفات الكائنات فلن ينجح هذا الأسلوب — لأن كائنين مختلفين يحملان نفس الحقول يبقيان قيمتين مختلفتين — لكنه الطريقة الاصطلاحية لإزالة التكرار من مصفوفة JavaScript تحوي نصوصًا أو أرقامًا أو قيمًا منطقية.
Set vs Array: متى تستبدل المصفوفة بـ Set؟
كلٌّ من المصفوفات و Set يحفظ مجموعة من القيم، فمتى نختار هذا ومتى نختار ذاك؟
استخدم Set عندما:
- تريد ضمان عدم تكرار القيم وترغب أن تتكفّل بيئة التشغيل بفرض ذلك تلقائيًا.
- تُجري عمليات تحقق كثيرة من وجود عنصر. فالاستدعاء
set.has(x)تعقيده O(1)، بينماarray.includes(x)تعقيده O(n). وداخل حلقة، يتضخم هذا الفارق سريعًا. - يكفيك ترتيب الإدراج فقط. إذ يمر Set على العناصر بترتيب إضافتها، لكنه لا يدعم الوصول بالفهرس.
وابقَ مع المصفوفة عندما:
- تحتاج الوصول بالموقع — مثل
arr[0]، أو التقطيع، أو الفرز. - التكرار له معنى — كسلة شراء فيها قطعتان من نفس المنتج.
- ستعتمد بكثرة على دوال المصفوفات مثل
.mapو.filterو.reduce. فهذه غير متاحة في Set، وستضطر لنشره داخل مصفوفة أولًا.
وإليك مثالًا سريعًا يوضّح الفارق من ناحية الأداء:
لو كانت banned مصفوفة، فإن كل استدعاء لـ filter سيضطر لفحص القائمة بالكامل في كل مرة. أما مع Set، فالبحث يتم بزمن ثابت.
التكرار على Set
القصة هي نفسها مع Map — حلقة for...of تعمل مباشرة، ونشر القيم باستخدام المعامل ... يعطيك مصفوفة جاهزة:
توفّر Set أيضاً الدوال .keys() و .values() و .entries() لتتناسق مع Map، رغم أنّ المفاتيح والقيم في Set هي الشيء نفسه. لكن في الغالب ستتعامل مباشرة مع التكرار عليها.
مثال تطبيقي: حساب الزوّار الفريدين لكل صفحة
لنجمع بين الاثنين — Map تربط مسار كل صفحة بـ Set يحتوي على معرّفات الزوّار:
الـ Map مسؤول عن ربط المسار بالمجموعة، والـ Set يتكفّل بحذف التكرارات داخل كل مجموعة. صحيح أنه يمكنك تنفيذ نفس الفكرة باستخدام كائن عادي ومصفوفات، لكنك ستضطر لكتابة فحوصات indexOf وhasOwnProperty في كل مكان.
نظرة سريعة على WeakMap و WeakSet
هناك نوعان قريبان من هذه المجموعات مخصصان لحالة استخدام ضيقة: WeakMap و WeakSet. يحتفظ كلاهما بالمراجع بشكل ضعيف، أي أن أي عنصر يُصبح مفتاحه (في حالة WeakMap) أو قيمته (في حالة WeakSet) بدون أي مراجع أخرى، يُحذف تلقائيًا بواسطة جامع المهملات.
لا تقبل هذه البنى إلا الكائنات كمفاتيح، وهي غير قابلة للتكرار، ولا تملك خاصية .size. وهذا تصميم مقصود — لأنك لو استطعت التكرار عليها، لأصبح عمل الـ garbage collector قابلاً للملاحظة. تُستخدم هذه البنى عادةً في تخزين بيانات وصفية مؤقتة عن كائنات لا تملكها أنت، ونادراً ما تصادفها في الكود اليومي.
الخطوة التالية: JSON
Map و Set ممتازتان داخل الذاكرة، لكن أياً منهما لا تنجو من JSON.stringify سليمةً — إذ يتحول الـ Map إلى {} وكذلك الـ Set إلى {}. الصفحة التالية مخصصة لـ JSON: كيف تُحوِّل البيانات إلى نصٍ وتُعيد تحليلها، والأنماط المتّبعة للتعامل مع المجموعات التي تعرفنا عليها هنا حين تحتاج لعبور الشبكة أو الحفظ في ملف.
الأسئلة الشائعة
ما الفرق بين Map و Object في JavaScript؟
الـ Map يقبل أي قيمة كمفتاح — كائنات، دوال، أرقام، أي شيء — بينما الـ Object يحوّل المفاتيح تلقائياً إلى نصوص (أو رموز Symbols). كذلك يوفّر Map الخاصية .size لمعرفة عدد العناصر مباشرة، ويحافظ على ترتيب الإدخال عند التكرار، ولا يرث مفاتيح من الـ prototype، فلا تقلق من تعارض مفاتيحك مع toString أو constructor. استخدم Map عندما تكون المفاتيح ليست نصوصاً، أو عندما تحتاج إلى إضافة وحذف متكرر للعناصر.
ما فائدة Set في JavaScript؟
الـ Set يخزّن قيماً فريدة فقط، ويتجاهل التكرارات بصمت. وأسرع طريقة لإزالة التكرار من مصفوفة هي [...new Set(arr)]. كما يمنحك الـ Set عملية .has() بسرعة O(1)، وهذا أسرع بكثير من array.includes() عند التحقق من وجود عنصر داخل حلقة.
كيف أمرّ على عناصر Map بالتكرار؟
يمكنك استخدام for...of مباشرة: for (const [key, value] of myMap) لتفكيك كل عنصر إلى مفتاح وقيمة. وبإمكانك أيضاً التكرار عبر myMap.keys() أو myMap.values() أو myMap.entries(). الترتيب مضمون بحسب الإدخال، عكس الكائنات العادية التي لا تضمن هذا الترتيب دائماً خصوصاً مع المفاتيح ذات الشكل الرقمي.