EduFlow
أساسيات JavaScript
⁦0%⁩

الوحدة 3 · العمل مع البيانات

الجمع والترتيب: reduce وsort

18 دقراءة 4 دبيئة تجريب الكود

الدرس في لمحة

تلخّص قائمة في قيمة واحدة باستخدام reduce، وتبني مجاميع لكل فئة، وترتّب الأرقام والنصوص والكائنات ترتيبًا صحيحًا بدالة مقارنة، دون الوقوع في فخّي الترتيب الافتراضي والتعديل في المكان.

  1. 1reduce: قيم كثيرة تصبح قيمة واحدة
  2. 2sort: وضع الأشياء في ترتيبها

لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.

سؤالان على كل تطبيق ميزانية أن يجيب عنهما: “كم أنفقت على الطعام؟” و"ما أكبر مصاريفي؟". الأول يحوّل القائمة إلى رقم واحد لكل فئة. والثاني يضع القائمة في ترتيب مفيد. لدى JavaScript دالة لكل منهما، ولكل منهما فخّ واحد شهير.

reduce: قيم كثيرة تصبح قيمة واحدة

أنت تعرف نمط المُراكِم: ابدأ بقيمة، وحدّثها مع كل عنصر، واستخدمها في النهاية. وreduce هي هذا النمط على شكل دالة:

JavaScript
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 بالقيمة الابتدائية 0. الاستدعاء الأول يُرجع 0 زائد 450، أي 450؛ والثاني يُرجع 450 زائد 1220، أي 1670؛ والثالث يُرجع 1670 زائد 1350، أي 3020، وهي النتيجة النهائية. 0 ابتدائية 0 + 450 Coffee 450 + 1220 Train 1670 + 1350 Lunch 3020 تُرجع 450 تُرجع 1670
reduce تمرّر المُراكِم من استدعاء إلى الذي يليه

مرّر القيمة الابتدائية دائمًا. فدونها تستخدم reduce العنصر الأول مُراكِمًا ابتدائيًا، وهو هنا كائن مصروف كامل لا رقم، ومع القائمة الفارغة ترمي TypeError: Reduce of empty array with no initial value.

التجميع: reduce إلى كائن

يمكن أن يكون المُراكِم أي شيء، بما في ذلك كائن. وهكذا تبني مجاميع لكل فئة:

JavaScript
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:

JavaScript
const amounts = [450, 1220, 99];
amounts.sort();
console.log(amounts);

دون تعليمات، تحوّل sort كل عنصر إلى نص وترتّبها كنصوص، مثل الكلمات في القاموس. وبصفتها نصوصًا، تأتي “1220” قبل “450” لأن الحرف “1” يأتي قبل “4”. هذا جيد للكلمات وخاطئ للأرقام.

والحل دالة مقارنة (comparator): دالة تستلم عنصرين، a وb، وتُرجع رقمًا يقول أيّهما يأتي أولًا.

ما تُرجعه دالة المقارنة النتيجة
رقم سالب a تأتي قبل b
رقم موجب b تأتي قبل a
0 يبقى ترتيبهما الحالي

ومع الأرقام، الطرح يُنتج هذا بالضبط:

JavaScript
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 فتقارن كما يفعل قاموس لغة حقيقية:

JavaScript
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، ومدعومة في كل المتصفّحات الحالية) فتُرجع نسخة مرتّبة مباشرة، فلا تحتاج إلى تذكّر خطوة النسخ:

JavaScript
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 (أو رتّب نسخة) حين يجب أن يبقى الأصل كما هو.

قراءات إضافية

تحقّق من فهمك

4 أسئلة

السؤال 1 من 4

ماذا تُرجع [450, 1220, 99].sort()؟

السؤال 2 من 4

أيّ دالة مقارنة ترتّب المصاريف من الأغلى إلى الأرخص؟

السؤال 3 من 4

استُدعيت [].reduce((sum, e) => sum + e.amount) على قائمة فارغة. ماذا يحدث؟

السؤال 4 من 4

تعرض المصاريف مرتّبةً حسب المبلغ في لوحة، وبترتيب إضافتها في لوحة أخرى. واللوحتان تقرآن المصفوفة نفسها expenses. ما الذي يفسد مع expenses.sort(byAmount)؟

تدريب

بيئة تجريب الكود

المجاميع والترتيب

أكمل أربع دوال لشاشة الملخّص في Pocket:

  • totalCents(expenses) تُرجع مجموع كل قيم amount (و0 للقائمة الفارغة). استخدم reduce.
  • totalsByCategory(expenses) تُرجع كائنًا فيه خاصية لكل فئة تحمل مجموع تلك الفئة، مثل { food: 1800, travel: 1220, fun: 99 }.
  • biggestFirst(expenses) تُرجع مصفوفة جديدة مرتّبة حسب amount، الأكبر أولًا.
  • alphabetical(expenses) تُرجع مصفوفة جديدة مرتّبة حسب label من A إلى Z، وتعامل “coffee” و"Coffee" على أنهما متماثلتان.

ولا يجوز لأيّ من دالتي الترتيب أن تعيد ترتيب المصفوفة التي تستلمها.

const expenses = [
  { label: "coffee", amount: 450, category: "food" },
  { label: "Train", amount: 1220, category: "travel" },
  { label: "Lunch", amount: 1350, category: "food" },
  { label: "Museum", amount: 99, category: "fun" },
];

// Sum of all amounts (0 for an empty list)
function totalCents(expenses) {
  return 0;
}

// An object like { food: 1800, travel: 1220, fun: 99 }
function totalsByCategory(expenses) {
  return {};
}

// A NEW array, most expensive first; do not reorder the original
function biggestFirst(expenses) {
  return expenses.sort();
}

// A NEW array sorted by label, A to Z, ignoring upper/lower case
function alphabetical(expenses) {
  return expenses;
}

console.log(totalCents(expenses), totalsByCategory(expenses));
جارٍ تحميل المحرّر…

Ctrl/⌘ + Enter للتشغيل · يعمل في بيئة معزولة داخل متصفّحك. لا شيء يغادر جهازك.

الـ console

(لا مخرجات)

الفحوص

  • totalCents تجمع المبالغ وتُرجع 0 للقائمة الفارغة
  • totalsByCategory تحسب مجموع كل فئة
  • biggestFirst ترتّب حسب المبلغ، الأكبر أولًا
  • alphabetical ترتّب حسب التسمية، متجاهلةً حالة الأحرف
  • تحتفظ المصفوفة الأصلية بترتيبها