من أين تأتي التكرارات ولماذا تمثل مشكلة؟
\nتخيل أنك تجمع بيانات من API، أو تحلل ملف CSV، أو تعالج مدخلات المستخدم. في 90% من الحالات، ستواجه مصفوفات تحتوي على نفس العنصر مكررًا عدة مرات. هذه ليست مجرد مشكلة جمالية — فالتكرارات يمكن أن تشوه الإحصائيات، وتعطل عرض القوائم، أو تؤدي إلى حلقات لا نهائية في الخوارزميات.
\nسؤال "إزالة القيم المكررة من مصفوفة JavaScript" على Stack Overflow حصد آلاف المشاهدات لسبب وجيه. إنها عملية أساسية ولكنها بالغة الأهمية يجب أن يعرفها كل مطور. اليوم سننظر تحت الغطاء ونحلل كيفية عمل الطرق المختلفة — من البسيطة إلى الأكثر كفاءة.
\n\nالطريقة رقم 1: Set — الأكثر أناقة وسرعة
\nإذا سألت مطور JavaScript حديث: "كيف تزيل التكرارات؟"، أول ما سيقوله هو Set. وهذا صحيح. ولكن كيف يعمل؟
const numbers = [1, 2, 2, 3, 4, 4, 5];\nconst uniqueNumbers = [...new Set(numbers)];\nconsole.log(uniqueNumbers); // [1, 2, 3, 4, 5]\n\nماذا يحدث تحت غطاء Set؟
\nكائن Set يخزن قيمًا فريدة من أي نوع. عندما تمرر مصفوفة إلى المُنشئ new Set(numbers)، يقوم محرك JavaScript (V8, SpiderMonkey إلخ) بتنفيذ الخطوات التالية:
- \n
- التكرار عبر المصفوفة: يمر المحرك على كل عنصر من المصفوفة الأصلية. \n
- التحقق من التفرد: لكل عنصر، يتم استدعاء دالة تجزئة داخلية. إذا كان العنصر موجودًا بالفعل في "السلة" (bucket)، يتم تجاهله. إذا لم يكن موجودًا، يتم إضافته. \n
- تجميع النتيجة: عامل الانتشار
[...set](spread) يفك الكائن القابل للتكرار Set ويعيده إلى مصفوفة. \n
ملاحظة مهمة: Set يستخدم خوارزمية المقارنة SameValueZero. هذا يعني أن NaN يُعتبر مساويًا لـ NaN (على عكس ===)، بينما +0 و -0 يُعتبران قيمتين مختلفتين. عمليًا، نادرًا ما يكون لهذا أهمية، لكن من الجيد معرفته.
أداء Set
\nتعقيد العملية هو O(n). يتم فحص كل عنصر مرة واحدة فقط. هذا هو أفضل مؤشر بين جميع الطرق. للمصفوفات التي تحتوي على 10,000 عنصر، يمكن أن يصل الفرق بين Set والطرق الأخرى إلى عشرات الميلي ثانية — وهو أمر بالغ الأهمية للتطبيقات عالية التحميل.
\n\nالطريقة رقم 2: filter + indexOf — الكلاسيكية
\nقبل ظهور ES6 (2015)، كان المطورون يستخدمون مزيجًا من filter و indexOf. لا تزال هذه الطريقة موجودة في الكود القديم (legacy code):
const numbers = [1, 2, 2, 3, 4, 4, 5];\nconst uniqueNumbers = numbers.filter((value, index, self) => {\n return self.indexOf(value) === index;\n});\nconsole.log(uniqueNumbers); // [1, 2, 3, 4, 5]\n\nكيف تعمل؟
\n- \n
filterيمر على كل عنصر في المصفوفة. \n - لكل عنصر، يتم استدعاء
indexOf(value)الذي يعيد أول فهرس توجد فيه هذه القيمة. \n - إذا كان فهرس العنصر الحالي يطابق أول فهرس تم العثور عليه — فهذا يعني أن هذا هو أول ظهور، ويتم الاحتفاظ بالعنصر. إذا لم يطابق — فهذا تكرار، ويتم تجاهله. \n
المزالق
\nالمشكلة الرئيسية هي الأداء. indexOf داخليًا يستخدم بحثًا خطيًا (O(n) لكل استدعاء). في النهاية، لمصفوفة من N عنصر، يصبح التعقيد O(n²). مع 1000 عنصر، لا يزال هذا مقبولاً، ولكن مع 100,000 عنصر، يصبح البطء ملحوظًا.
عيب آخر: indexOf لا يعمل بشكل صحيح مع NaN. إذا كانت المصفوفة تحتوي على NaN، فإن indexOf(NaN) سيعيد دائمًا -1، وسيتم إضافة العنصر مرة أخرى. للتحقق من NaN، تحتاج إلى استخدام