الوحدة 3 · العمل مع البيانات
الجمع والترتيب: reduce وsort
18 دقراءة 4 دبيئة تجريب الكود
الدرس في لمحة
تلخّص قائمة في قيمة واحدة باستخدام reduce، وتبني مجاميع لكل فئة، وترتّب الأرقام والنصوص والكائنات ترتيبًا صحيحًا بدالة مقارنة، دون الوقوع في فخّي الترتيب الافتراضي والتعديل في المكان.
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
سؤالان على كل تطبيق ميزانية أن يجيب عنهما: “كم أنفقت على الطعام؟” و"ما أكبر مصاريفي؟". الأول يحوّل القائمة إلى رقم واحد لكل فئة. والثاني يضع القائمة في ترتيب مفيد. لدى JavaScript دالة لكل منهما، ولكل منهما فخّ واحد شهير.
reduce: قيم كثيرة تصبح قيمة واحدة
أنت تعرف نمط المُراكِم: ابدأ بقيمة، وحدّثها مع كل عنصر، واستخدمها في النهاية. وreduce هي هذا النمط على شكل دالة:
const expenses = [
{ label: "Coffee", amount: 450, category: "food" },
{ label: "Train", amount: 1220, category: "travel" },
{ label: "Lunch", amount: 1350, category: "food" },
];
const total = expenses.reduce((sum, e) => sum + e.amount, 0);
console.log(total);تأخذ reduce وسيطين: callback وقيمة ابتدائية (هنا 0). وتستدعي الـ callback مرة لكل عنصر، مع المُراكِم (accumulator) الحالي (sum) والعنصر (e). وكل ما يُرجعه الـ callback يصبح المُراكِم في الاستدعاء التالي. وبعد العنصر الأخير، يكون المُراكِم النهائي هو النتيجة.
مرّر القيمة الابتدائية دائمًا. فدونها تستخدم reduce العنصر الأول مُراكِمًا ابتدائيًا، وهو هنا كائن مصروف كامل لا رقم، ومع القائمة الفارغة ترمي TypeError: Reduce of empty array with no initial value.
التجميع: reduce إلى كائن
يمكن أن يكون المُراكِم أي شيء، بما في ذلك كائن. وهكذا تبني مجاميع لكل فئة:
const expenses = [
{ label: "Coffee", amount: 450, category: "food" },
{ label: "Train", amount: 1220, category: "travel" },
{ label: "Lunch", amount: 1350, category: "food" },
];
const byCategory = expenses.reduce((totals, e) => {
totals[e.category] = (totals[e.category] ?? 0) + e.amount;
return totals;
}, {});
console.log(byCategory);مع كل مصروف، اقرأ المجموع الجاري لفئته (أو 0 إن كانت هذه أول مرة تظهر فيها الفئة؛ وللعامل ?? شرح خاص في الدرس التالي)، وأضف المبلغ، وخزّنه من جديد بالأقواس المربّعة، لأن اسم الخاصية في متغيّر. ثم أرجع الكائن نفسه كي يستلمه الاستدعاء التالي.
وحين تريد تجميع المصاريف نفسها بدلًا من مجموع لكل مجموعة، فهناك اختصار مدمج منذ عام 2024: Object.groupBy(expenses, (e) => e.category) تُرجع كائنًا خصائصه هي الفئات، وقيمه مصفوفات المصاريف المطابقة. إنها مدعومة في كل المتصفّحات الحالية وفي Node.js، وتُقرأ أفضل من مقابلها المكتوب بـ reduce. أما المجاميع فما زالت تحتاج إلى reduce (أو حلقة تمرّ على المجموعات).
reduce قوية، وهذا سبب لاستخدامها باعتدال. إن كانت map أو filter أو حلقة for...of قصيرة تقول الشيء نفسه بوضوح أكبر، ففضّلها. المجاميع والتجميع هي المواضع التي تُقرأ فيها reduce أفضل ما يكون.
sort: وضع الأشياء في ترتيبها
يبدو الترتيب سهلًا، لكنه يحتوي على أشهر فخّ في JavaScript:
const amounts = [450, 1220, 99];
amounts.sort();
console.log(amounts);دون تعليمات، تحوّل sort كل عنصر إلى نص وترتّبها كنصوص، مثل الكلمات في القاموس. وبصفتها نصوصًا، تأتي “1220” قبل “450” لأن الحرف “1” يأتي قبل “4”. هذا جيد للكلمات وخاطئ للأرقام.
والحل دالة مقارنة (comparator): دالة تستلم عنصرين، a وb، وتُرجع رقمًا يقول أيّهما يأتي أولًا.
| ما تُرجعه دالة المقارنة | النتيجة |
|---|---|
| رقم سالب | a تأتي قبل b |
| رقم موجب | b تأتي قبل a |
0 |
يبقى ترتيبهما الحالي |
ومع الأرقام، الطرح يُنتج هذا بالضبط:
const amounts = [450, 1220, 99];
console.log([...amounts].sort((a, b) => a - b)); // ascending
console.log([...amounts].sort((a, b) => b - a)); // descendingحين تكون a تساوي 450 وb تساوي 99، يكون a - b موجبًا، فتنتقل 99 إلى الأمام. واقلبها إلى b - a ليأتي الأكبر أولًا.
النصوص تحتاج إلى مقارنة خاصة بها، لأن < تقارن رموز الحروف وتضع كل حرف كبير قبل كل حرف صغير. أما localeCompare فتقارن كما يفعل قاموس لغة حقيقية:
const labels = ["lunch", "Train", "coffee", "Éclair"];
console.log(labels.toSorted((a, b) => a.localeCompare(b)));والكائنات تُرتَّب حسب إحدى خصائصها، بالأنماط نفسها: (a, b) => b.amount - a.amount ليأتي الأغلى أولًا، و(a, b) => a.label.localeCompare(b.label) للترتيب الأبجدي.
sort تغيّر الأصل
تعيد sort ترتيب المصفوفة في مكانها وتُرجع المصفوفة نفسها. فإن كان كود آخر يقرأ القائمة، ربما ليعرض المصاريف بترتيب إضافتها، فقد صارت مرتّبة عنده أيضًا. ولهذا ترتّب الأمثلة أعلاه نسخة، [...amounts]. أما الدالة الأحدث toSorted (جزء من JavaScript منذ عام 2023، ومدعومة في كل المتصفّحات الحالية) فتُرجع نسخة مرتّبة مباشرة، فلا تحتاج إلى تذكّر خطوة النسخ:
const expenses = [
{ label: "Coffee", amount: 450 },
{ label: "Train", amount: 1220 },
{ label: "Lunch", amount: 1350 },
];
const biggestFirst = expenses.toSorted((a, b) => b.amount - a.amount);
console.log(biggestFirst.map((e) => e.label), expenses.map((e) => e.label));في الدرس التالي تتعامل مع بيانات غير موجودة أصلًا: خصائص مفقودة، وقيم فارغة، ومجموعتان، Map وSet، صُمّمتا للبحث السريع ولضمان عدم التكرار.
أهم النقاط
reduceتمرّ على القائمة حاملةً مُراكِمًا (accumulator)؛ والـ callback يُرجع المُراكِم التالي، والمُراكِم الأخير هو النتيجة.- مرّر دائمًا قيمة ابتدائية إلى
reduce، مثل0للمجاميع أو{}للتجميع؛ فدونها ترمي القائمة الفارغة خطأ. - دون دالة مقارنة، تقارن
sortالعناصر على أنها نصوص، فتُرتَّب[450, 1220, 99]لتصبح[1220, 450, 99]. - دالة المقارنة تُرجع رقمًا سالبًا لتضع
aأولًا، وموجبًا لتضعbأولًا؛ و(a, b) => a - bترتّب الأرقام تصاعديًا. sortتعدّل المصفوفة في مكانها؛ استخدمtoSorted(أو رتّب نسخة) حين يجب أن يبقى الأصل كما هو.