Menu
العربية

Map و Set في JavaScript: متى تستخدمهما بدل Object و Array؟

تعرّف على كيفية عمل Map و Set في JavaScript، والفرق بينهما وبين الكائنات والمصفوفات العادية، ومتى يكون استخدامهما هو الخيار الأفضل.

مجموعتان تتجاوزان Object و Array

الكائنات العادية والمصفوفات تكفي لمعظم ما يحتاجه أي برنامج JavaScript، لكنها لم تُصمَّم لكل المهام. هنا يأتي دور Map و Set في JavaScript: مجموعتان مدمجتان في اللغة تسدّان ثغرتين محددتين، وهما البحث بمفاتيح ليست نصية، والتحقق السريع من وجود عنصر بدون تكرار.

هاتان البنيتان موجودتان في اللغة منذ إصدار ES2015. كلاهما قابل للتكرار (iterable)، ولكل منهما خاصية .size، ويتعاملان جيدًا مع عامل الانتشار (spread operator). الفكرة ببساطة:

  • Map — مثل الكائن، لكن المفاتيح يمكن أن تكون من أي نوع، وترتيب الإدخال محفوظ.
  • Set — مثل المصفوفة، لكن القيم فريدة والبحث فيها سريع.

إنشاء Map واستخدامه

يحتفظ Map بأزواج من المفاتيح والقيم. تُنشئه عبر new Map()، وتتعامل معه من خلال .set() و .get() و .has() و .delete():

index.js
Output
Click Run to see the output here.

يمكنك أيضًا تمرير مصفوفة من أزواج [key, value] إلى المُنشئ (constructor) لتعبئتها بقيم ابتدائية:

index.js
Output
Click Run to see the output here.

شكل المصفوفة ثنائية العناصر هذا ستلاقيه في كل مكان مع الـ Map — لأنه ببساطة الطريقة اللي تتمثل بها المدخلات (entries) أثناء التكرار.

الفرق بين Map و Object: ليش نستخدم Map أصلاً؟

الكائنات العادية (Objects) تبدو وكأنها تؤدي نفس الغرض، وفي معظم الحالات فعلاً تؤديه. لكن Map يعالج بعض النقاط المزعجة تحديداً:

index.js
Output
Click Run to see the output here.

الكائنات (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) بسهولة:

index.js
Output
Click Run to see the output here.

إذا كنت تحتاج المفاتيح فقط أو القيم فقط، استخدم .keys() أو .values(). وتتوفر أيضًا .forEach() إذا كنت تفضّلها:

index.js
Output
Click Run to see the output here.

لتحويل الـ Map إلى كائن عادي أو مصفوفة، استخدم عامل النشر (spread):

index.js
Output
Click Run to see the output here.

إنشاء Set واستخدامه في JavaScript

تخزّن بنية Set في جافا سكريبت قيمًا فريدة فقط، فإذا حاولت إضافة قيمة موجودة مسبقًا فلن يحدث شيء:

index.js
Output
Click Run to see the output here.

تحديد تفرد العناصر يعتمد على نفس قاعدة المساواة الصارمة ===، مع استثناء بسيط: قيمة NaN تُعتبر مساوية لنفسها داخل الـ Set، رغم أن NaN === NaN تُرجع false في أي مكان آخر.

يمكنك تمرير أي عنصر قابل للتكرار (iterable) إلى الـ constructor لتعبئة الـ Set مباشرة، ومن هنا تأتي حيلة إزالة التكرار من مصفوفة JavaScript:

index.js
Output
Click Run to see the output here.

سطر واحد، ويصلح مع أي نوع بدائي. أما مع مصفوفات الكائنات فلن ينجح هذا الأسلوب — لأن كائنين مختلفين يحملان نفس الحقول يبقيان قيمتين مختلفتين — لكنه الطريقة الاصطلاحية لإزالة التكرار من مصفوفة JavaScript تحوي نصوصًا أو أرقامًا أو قيمًا منطقية.

Set vs Array: متى تستبدل المصفوفة بـ Set؟

كلٌّ من المصفوفات و Set يحفظ مجموعة من القيم، فمتى نختار هذا ومتى نختار ذاك؟

استخدم Set عندما:

  • تريد ضمان عدم تكرار القيم وترغب أن تتكفّل بيئة التشغيل بفرض ذلك تلقائيًا.
  • تُجري عمليات تحقق كثيرة من وجود عنصر. فالاستدعاء set.has(x) تعقيده O(1)، بينما array.includes(x) تعقيده O(n). وداخل حلقة، يتضخم هذا الفارق سريعًا.
  • يكفيك ترتيب الإدراج فقط. إذ يمر Set على العناصر بترتيب إضافتها، لكنه لا يدعم الوصول بالفهرس.

وابقَ مع المصفوفة عندما:

  • تحتاج الوصول بالموقع — مثل arr[0]، أو التقطيع، أو الفرز.
  • التكرار له معنى — كسلة شراء فيها قطعتان من نفس المنتج.
  • ستعتمد بكثرة على دوال المصفوفات مثل .map و .filter و .reduce. فهذه غير متاحة في Set، وستضطر لنشره داخل مصفوفة أولًا.

وإليك مثالًا سريعًا يوضّح الفارق من ناحية الأداء:

index.js
Output
Click Run to see the output here.

لو كانت banned مصفوفة، فإن كل استدعاء لـ filter سيضطر لفحص القائمة بالكامل في كل مرة. أما مع Set، فالبحث يتم بزمن ثابت.

التكرار على Set

القصة هي نفسها مع Map — حلقة for...of تعمل مباشرة، ونشر القيم باستخدام المعامل ... يعطيك مصفوفة جاهزة:

index.js
Output
Click Run to see the output here.

توفّر Set أيضاً الدوال .keys() و .values() و .entries() لتتناسق مع Map، رغم أنّ المفاتيح والقيم في Set هي الشيء نفسه. لكن في الغالب ستتعامل مباشرة مع التكرار عليها.

مثال تطبيقي: حساب الزوّار الفريدين لكل صفحة

لنجمع بين الاثنين — Map تربط مسار كل صفحة بـ Set يحتوي على معرّفات الزوّار:

index.js
Output
Click Run to see the output here.

الـ Map مسؤول عن ربط المسار بالمجموعة، والـ Set يتكفّل بحذف التكرارات داخل كل مجموعة. صحيح أنه يمكنك تنفيذ نفس الفكرة باستخدام كائن عادي ومصفوفات، لكنك ستضطر لكتابة فحوصات indexOf وhasOwnProperty في كل مكان.

نظرة سريعة على WeakMap و WeakSet

هناك نوعان قريبان من هذه المجموعات مخصصان لحالة استخدام ضيقة: WeakMap و WeakSet. يحتفظ كلاهما بالمراجع بشكل ضعيف، أي أن أي عنصر يُصبح مفتاحه (في حالة WeakMap) أو قيمته (في حالة WeakSet) بدون أي مراجع أخرى، يُحذف تلقائيًا بواسطة جامع المهملات.

index.js
Output
Click Run to see the output here.

لا تقبل هذه البنى إلا الكائنات كمفاتيح، وهي غير قابلة للتكرار، ولا تملك خاصية .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(). الترتيب مضمون بحسب الإدخال، عكس الكائنات العادية التي لا تضمن هذا الترتيب دائماً خصوصاً مع المفاتيح ذات الشكل الرقمي.

تعلّم البرمجة مع Coddy

ابدأ الآن