نادراً ما يعني العمل مع المصفوفات في JavaScript أنك بحاجة إلى كتابة حلقة for بعد الآن. توفر لك اللغة الآن أدوات تعبيرية تحول البيانات المتداخلة والمزدحمة إلى عمليات تحويل نظيفة وسهلة القراءة. يكمل هذا الدليل ما بدأته الأساسيات، مع التركيز على التسطيح (flattening)، والتقليص (reducing)، والاستعلام (querying)، والأنماط الحديثة غير القابلة للتغيير (immutable patterns) التي تحافظ على بياناتك الأصلية دون مساس.

تسطيح الهياكل المتداخلة باستخدام flat() و flatMap()

البيانات في العالم الحقيقي تحب التداخل. سواء كنت تسحب سجلات من واجهة برمجة تطبيقات (API)، أو تحلل ملف CSV، أو تقرأ ملف إعدادات، فغالباً ما ستنتهي بمصفوفات داخل مصفوفات. وُجدت flat() لطيّ تلك الطبقات وتحويلها إلى قائمة خطية واحدة.

استدعِ flat() على مصفوفة متداخلة، وستقوم افتراضياً بتسطيح مستوى واحد فقط:

const nested = [[1, 2], [3, 4]];
const flat = nested.flat();
// [1, 2, 3, 4]

تبقى المصفوفة الأصلية nested كما هي تماماً. إذا كان التداخل أعمق، فقم بتمرير وسيط العمق مثل flat(2) أو حتى flat(Infinity) عندما لا تعرف عدد الطبقات الموجودة. هذا أفضل بكثير من كتابة دوال مساعدة تكرارية (recursive) يدوياً.

عندما تحتاج إلى تحويل العناصر وتسطيح النتيجة في آن واحد، يقوم flatMap() بكليهما في تمريرة واحدة. فهو يقوم بتشغيل دالة تحويل (mapping function) على كل عنصر، ثم يسطح المصفوفات الناتجة بمستوى واحد. القيام بذلك باستخدام استدعاءات منفصلة مثل .map().flat() يعمل، لكن flatMap() يتخطى المصفوفة الوسيطة، مما يوفر الذاكرة وتكراراً ثانياً. حالة استخدام كلاسيكية هي تقسيم مصفوفة من الجمل إلى كلمات فردية:

const lines = ['hello world', 'foo bar'];
const words = lines.flatMap(line => line.split(' '));
// ['hello', 'world', 'foo', 'bar']

التصفية والتحويل: اعرف الفرق

تنشئ filter() مصفوفة جديدة تحتوي فقط على العناصر التي تجتاز اختبارك. يجب أن تعيد دالة الاستدعاء (callback) قيمة truthy للإبقاء على العنصر، أو قيمة falsy لحذفه. وهي لا تقوم أبداً بتغيير المصفوفة المصدر.

const users = [
  { name: 'Ajay', active: true },
  { name: 'Bina', active: false }
];
const activeUsers = users.filter(u => u.active);

من السهل الخلط بين filter() و map() لأن كليهما يعيد مصفوفات جديدة ويقبل دوال استدعاء. الفرق يكمن في طبيعة عمل كل منهما؛ حيث تعيد map() مصفوفة جديدة بنفس الطول. إذا قمت بتحويل كل عنصر وأجريت فحصاً شرطياً، فسترى بالفعل true أو false لكل خانة، لأن map() لا تحذف العناصر أبداً، بل تغيرها فقط. من ناحية أخرى، تعيد filter() مصفوفة قد تكون أقصر تحتوي فقط على العناصر التي تجتاز اختبارك. إذا كنت تريد إزالة البيانات، فاستخدم filter(). أما إذا كنت تريد تحويل كل قطعة من البيانات مع الحفاظ على العدد، فاستخدم map().

تكثيف المصفوفات باستخدام reduce()

بينما تعيد map() و filter() مصفوفات، تقوم reduce() بتكثيف قائمة كاملة إلى قيمة واحدة. فهي تتحرك من اليسار إلى اليمين، حاملةً "مراكماً" (accumulator) عبر كل خطوة.

const totals = [10, 20, 30];
const sum = totals.reduce((acc, current) => acc + current, 0);
// 60

الوسيط الثاني — وهو 0 في المثال أعلاه — هو القيمة الأولية للمراكم. إذا تجاهلته، فسيستخدم JavaScript العنصر الأول كنقطة انطلاق، ويبدأ التكرار من الفهرس (index) 1. هذا الأمر يربك الكثيرين عندما تكون المصفوفة فارغة، لأن استدعاء reduce() بدون قيمة أولية على مصفوفة فارغة يؤدي إلى خطأ من نوع TypeError.

reduce ليست للحسابات الرياضية فقط. يمكنك استخدامها لبناء جداول التكرار، أو دمج الكائنات، أو إيجاد القيم القصوى، أو تمرير البيانات عبر عمليات تحويل متعددة. ولأنها مرنة للغاية، فمن الأفضل إبقاء دالة الاستدعاء صغيرة وتسمية متغيراتك بوضوح؛ فكتلة reduce() الكثيفة تصبح بسرعة أصعب في القراءة من الحلقة التي استبدلتها.

الاستعلامات المنطقية: every() و some()

أحياناً لا تحتاج إلى مصفوفة جديدة؛ بل تحتاج إلى إجابة بنعم أو لا حول المجموعة. تعيد every() قيمة true فقط إذا كانت جميع العناصر تستوفي شرطك. وإذا فشل أول عنصر، فإنها تتوقف فوراً وتعيد false.

const scores = [85, 92, 78];
const allPassed = scores.every(s => s >= 50);

أما some() فهي الصورة المعاكسة؛ حيث تعيد true إذا نجح عنصر واحد على الأقل في الاختبار. وبمجرد العثور على تطابق، تتوقف العملية فوراً (short-circuit).

const hasPerfectScore = scores.some(s => s === 100);

تحل هذه الطرق محل الحلقات اليدوية لعمليات التحقق. هل تحتاج إلى التأكد من أن كل حقل في النموذج ممتلئ؟ استخدم every(). هل تريد معرفة ما إذا كان أي مستخدم يمتلك صلاحيات مسؤول؟ استخدم some().

الفحص والتحديث دون تغيير الأصل (Mutation)

يميل JavaScript الحديث بقوة نحو عدم القابلية للتغيير (immutability). تساعدك هذه الطرق على فحص المصفوفات أو تحديثها دون المساس بالأصل.

تعيد keys() كائناً تكرارياً (iterator) يحتوي على فهارس المصفوفة. وتعيد entries() تكراراً لأزواج من [index, value]. وتبرز قوتهما في حلقات for...of عندما تريد استخدام تفكيك البنية (destructuring) بشكل نظيف:

const fruits = ['apple', 'banana'];
for (const [index, fruit] of fruits.entries()) {
  console.log(index, fruit);
}

with() هي إضافة أحدث إلى اللغة، حيث تقوم باستبدال عنصر في فهرس (index) محدد وتُرجع مصفوفة جديدة تماماً. إنها تلبي نفس الحاجة التي يلبيها التعيين باستخدام الأقواس — arr[index] = value — باستثناء أنها تترك المصفوفة الأصلية دون تغيير. في أطر العمل مثل React، حيث يؤدي التغيير المباشر (mutation) إلى كسر عملية اكتشاف التغييرات، تُعد with() المسار الأكثر أماناً:

const original = [10, 20, 30];
const updated = original.with(1, 99);
// original: [10, 20, 30]
// updated:  [10, 99, 30]

Spread و Rest: اتجاهان لـ ...

تشترك صيغة الـ spread ومعامل الـ rest في نفس النقاط الثلاث، لكنهما ينقلان البيانات في اتجاهين متعاكسين.

استخدم عامل الـ spread لتوسيع مصفوفة إلى عناصر فردية. إنها الطريقة الأنظف لدمج المصفوفات أو استنساخها:

const front = [1, 2];
const back = [3, 4];
const merged = [...front, ...back];

const copy = [...merged];

يمكنك أيضاً إضافة عناصر إضافية بسهولة: [0, ...front, 99, ...back].

يقوم معامل الـ rest بتجميع عناصر متعددة في مصفوفة واحدة. وداخل توقيع الدالة (function signature)، فإنه يجمع أي وسائط (arguments) متبقية:

function logFirst(first, ...rest) {
  console.log('First:', first);
  console.log('Rest:', rest);
}
logFirst('a', 'b', 'c');
// First: a
// Rest: ['b', 'c']

معاً، يقومان بالمهمة الشائعة المتمثلة في قبول مدخلات متغيرة العدد (variadic input)، ومعالجتها كمصفوفة، ثم إعادة توزيعها في استدعاءات الدوال أو هياكل بيانات جديدة.

الخلاصة الحقيقية

الهدف من هذه الأساليب ليس إبهار زملائك في الفريق بأسطر برمجية مختصرة (one-liners)، بل هو جعل نيتك واضحة. عندما يرى مطور آخر filter()، سيعرف أنك تقوم بإزالة العناصر. وعندما يرى reduce()، سيعرف أنك تقوم باختزال البيانات. وعندما يرى with()، سيعرف أن المصفوفة الأصلية لا تزال سليمة في مكان ما. إن إتقان هذه الأدوات يعني كتابة كود يشرح نفسه، وهذا يوفر وقتاً أكثر بكثير مما قد توفره أي حلقة (loop) ذكية.