شرح المصفوفات (Arrays) في JavaScript

تخيل أن المصفوفة (Array) مثل "خزانة أدراج مرقمة". كل درج له رقم، وتضع فيه ألعابك. المهم أنك تعرف في أي درج وضعت اللعبة لتسترجعها بسهولة.

01

تعريف المصفوفة كائن خاص ومرتب (Ordered & Indexed Object)

📦
تخيل المصفوفة كقائمة أو خزانة ذات أدراج مرقمة، حيث يمكنك وضع أي نوع من البيانات في كل درج (أرقام، نصوص، كائنات، وحتى دوال).
📦
المصفوفة هي "كائن" (Object) عملاق، يحتوي على قطع صغيرة بداخله.

ما يميزها هو النظام الصارم: اللعبة الأولى تظل في المكان الأول، والثانية في المكان الثاني.

خصائص المصفوفة:

  • مرتبة (Ordered): العناصر لها ترتيب محدد يبدأ من الفهرس (Index) صفر.
  • ديناميكية (Dynamic): حجمها يتغير تلقائياً عند الإضافة أو الحذف.
  • غير محددة النوع: يمكنها تخزين أنواع مختلفة في نفس المصفوفة.
  • تمرير بالمرجع (Pass by Reference): تغيير النسخة قد يؤثر على الأصل.
// هذا مثال لمصفوفة بسيطة
// قمنا بإنشاء خزانة اسمها "toys" ووضعنا فيها 3 ألعاب
const toys = ["Car", "Doll", "Ball"];
                
// طبعاً المصفوفة نفسها تعتبر "كائن" (Object) لكنه ذكي ومرتب
console.log(toys); // سيطبع: ["Car", "Doll", "Ball"]
02

إنشاء المصفوفة طرق بناء الخزانة (Creation Methods)

🛠️
هناك طريقتان لصناعة الخزانة (المصفوفة). واحدة جيدة جداً والأخرى تحتاج حذر.

عند إنشاء مصفوفة، نريد أن نكون واضحين جدًا بشأن المعنى. الطريقة الأولى [ ] تُنشئ مصفوفة حقيقية مع عناصر فعليّة فوراً، بينما new Array(5) يفسّر الرقم كطول فقط وليس كقيمة أولية. هذه اللمسة الصغيرة تغيّر سلوك المصفوفة بالكامل، لذلك من السهل أن نخلط بين الطريقتين إذا لم ننتبه.

الطريقة الأولى: الأقواس [ ]

الأفضل والأكثر أماناً، كأنك تجمع أدواتك في حقيبة بسرعة.

// استخدمنا const لإنشاء ثابت اسمه myFavColors
// القوسين [ ] يخبران الكمبيوتر: "اصنع لي مصفوفة جديدة"
const myFavColors = ["Red", "Blue", "Green"];
                        
// الآن لدينا مصفوفة جاهزة ومليئة بالألوان
console.log(myFavColors);  // Red  Blue  Green 

⚠️ الطريقة الثانية: new Array()

تحتاج حذر! الكمبيوتر يلعب بنا لعبة عندما نمرر رقماً واحداً.

const riskyArray = new Array(5); 
// [empty × 5] (خمس خانات فارغة)
    
const correctArray = new Array();
correctArray[0] = 5; // النتيجة: [5]
03

الفهرسة (Indexing) العد يبدأ من الصفر

هذا أهم سر في البرمجة! في حياتنا نعد: 1، 2، 3... لكن في الكمبيوتر نعد: 0، 1، 2... اللعبة الأولى في المكان رقم 0. عندما نكتب arr[0] فإننا نطلب العنصر الأول، و arr[1] هو الثاني، وهكذا. هذا النظام يفرض أن الوصول إلى آخر عنصر يكون غالباً عبر arr.length - 1، أو عبر arr.at(-1) في النسخ الحديثة.

إذا طلبت فهرساً غير موجود، لا يحدث خطأ في معظم الحالات. ستُرجع JavaScript undefined لأن المكان لا يوجد فعلياً. هذا سلوك مهم لأن كثيراً من المبتدئين يظنون أن الوصول إلى فهرس غير موجود يسبب استثناء، لكنه في الواقع عادةً يكون undefined.

const animals = ["Cat", "Dog", "Lion"];
            
// للوصول لأي لعبة، نكتب اسم المصفوفة ثم رقم مكانها
console.log(animals[0]); // "Cat" (الحيوان الأول)
console.log(animals[1]); // "Dog" (الحيوان الثاني)
console.log(animals[2]); // "Lion" (الحيوان الثالث)
            
console.log(animals[animals.length - 1]); // الوصول لآخر عنصر

// ماذا لو طلبنا مكان غير موجود؟
console.log(animals[10]); // undefined
04

الخاصية length طول المصفوفة

length يعطيك "رقم" يخبرك كم مكان في الخزانة. وهو يساوي آخر فهرس + 1، لأن الفهرس يبدأ من الصفر. هذا يعني أنه إذا كانت المصفوفة فيها ثلاثة عناصر، فـ length تساوي 3، بينما آخر فهرس هو 2. والشيء المهم هنا هو أن length قابل للتغيير، لذلك يمكننا توسيع أو تقليص المصفوفة يدويًا، لكن هذا لا يعني أننا نضيف عناصر حقيقية دائماً؛ أحياناً نترك فراغات.

سحر تغيير الحجم

const numbers = [10, 20, 30, 40];
                
// length تسأل المصفوفة: كم طولك؟
console.log(numbers.length); // 4
                
// --- السحر: تغيير الحجم ---
                
// 1. تكبير المصفوفة
numbers.length = 6;
// النتيجة: [10, 20, 30, 40, empty × 2]
                
// 2. تصغير المصفوفة (حذف نهائي!)
numbers.length = 2;
// النتيجة: [10, 20]
05

التحقق من النوع Array.isArray() مقابل typeof

typeof

أداة "غبية" تقول عن المصفوفة "object" فقط.

Array.isArray()

أداة "ذكية" تعرف المصفوفة بدقة مطلقة.

عند التعامل مع القيم من مصادر مختلفة، نحتاج أحياناً إلى معرفة: "هل هذا فعلاً مصفوفة؟" لأن typeof لا يكفي. في JavaScript, المصفوفة هي object من ناحية النوع العام، لذلك لو أردنا اختبارها بدقة نستعمل Array.isArray(). هذا مهم عندما نتحقق من بيانات المستخدم، أو عند كتابة دوال تأخذ مصفوفة كمدخل وتعمل عليها.

const myArr = [1, 2, 3];
const myObj = { name: "Ali" };
            
// 1. تجربة typeof (الأداة الغبية)
console.log(typeof myArr);  // "object" ❌
console.log(typeof myObj);  // "object" ❌
            
// 2. تجربة Array.isArray() (الأداة الذكية)
console.log(Array.isArray(myArr)); // true ✅
console.log(Array.isArray(myObj)); // false ✅
Array.from()

تحويل النصوص أو الـ NodeList إلى مصفوفة حقيقية.


let chars = Array.from("Hello"); // ['H','e','l','l','o']
06

الإنشاء المتقدم Array literals و Array.from() و Array.of()

هناك أكثر من طريقة لإنشاء مصفوفة، وكل طريقة لها سلوك مختلف. هذا مهم لأن بعض الطرق تترك "فراغات" في المصفوفة، بينما أخرى تنشئ عناصر فعليّة. فالـ Array literal واضح ومباشر، أما new Array() فله سلوك خاص عند تمرير رقم واحد فقط، ويحمل في هذا الحالة معنى الطول وليس معنى العنصر. أما Array.from() فمفيدة عندما نريد تحويل شيء قابل للتكرار مثل النص أو قائمة DOM إلى مصفوفة حقيقية.

1

Array literal

const numbers = [1, 2, 3]; // إنشاء مصفوفة مباشرة مع القيم
const empty = []; // مصفوفة فارغة
const mix = [1, "Ali", true, { name: "Eslam" }]; // أنواع مختلفة داخل نفس المصفوفة
2

new Array()

const a = new Array(3); // ينتج ثلاث فراغات، لا ثلاث عناصر
console.log(a.length); // 3
console.log(a[0]); // undefined

const b = new Array(1, 2, 3); // هنا يتم إنشاء 3 عناصر فعليّة

Array.of() و Array.from()

const arrOf = Array.of(1, 2, 3); // ينشئ مصفوفة من القيم مباشرة
const letters = Array.from("Java"); // ['J', 'a', 'v', 'a']
const numbers2 = Array.from({ length: 4 }, (_, i) => i + 1); // [1, 2, 3, 4]
07

الفهرسة المتقدمة القراءة، الكتابة، آخر عنصر، at()

في JavaScript، الوصول إلى آخر عنصر بطريقة تقليدية هو: arr[arr.length - 1]. لكن اليوم توجد طريقة أكثر وضوحاً: arr.at(-1). الفكرة هنا أن الرقم السالب في at() يعني: "ابدأ من آخر العنصر"، وهذا يحول مفهوم الوصول من الحساب اليدوي إلى قراءة أسهل وأكثر قابلية للفهم.

هذا مفيد جداً عندما نريد الوصول إلى آخر عنصر أو الثاني من آخر بدون حساب يدوي. كما أنه يمنع الأخطاء الشائعة عند التعامل مع المصفوفات الطويلة أو عند كتابة منطق يعتمد على آخر قيمها. الفهرس السالب لا يعمل مع الوصول العادي arr[-1]؛ العمل الحقيقي يكون عبر at().

const colors = ["Red", "Green", "Blue"]; // مصفوفة بثلاثة ألوان

console.log(colors[0]); // Red
console.log(colors[colors.length - 1]); // Blue
console.log(colors.at(-1)); // Blue
console.log(colors.at(-2)); // Green
console.log(colors[99]); // undefined لأن الفهرس غير موجود
08

length وملء الفراغات ماذا يحدث عند تغيير طول المصفوفة؟

مفهوم length لا يقتصر على "عدد العناصر المملوءة" فقط؛ بل يمكن أن يكون أكبر من ذلك إذا تركنا فراغات. هذا يحدث عندما تغير طول المصفوفة إلى رقم أكبر، أو عندما تستخدم new Array(3). في هذه الحالة لا توجد قيم حقيقية في الأماكن الفارغة، لذلك قد يخلط المبتدئون بين "فراغ" و "undefined". الفرق مهم: الفراغ يعني أن المكان لا يحتوي على قيمة فعليّة، لكن الخاصية نفسها موجودة في المصفوفة.

const arr = [10, 20, 30]; // طولها 3
arr.length = 5; // تزيد الطول وتترك فراغات
console.log(arr); // [10, 20, 30, <empty>, <empty>]

arr.length = 2; // تقطع المصفوفة من آخرها
console.log(arr); // [10, 20]

const sparse = new Array(3); // مصفوفة فيها 3 فراغات
console.log(sparse.length); // 3
console.log(0 in sparse); // false لأن هذا فراغ لا قيمة حقيقية
09

الإضافة والإزالة push, pop, shift, unshift, splice

هذه الطرق تغير المصفوفة الأصلية مباشرة. بعض الطرق تعطيك قيمة مرتجعة مثل العدد الجديد أو العنصر المحذوف، وأخرى تعطيك مصفوفة جديدة. من هنا يأتي فرق أساسي بين push و slice: الأولى تعدّل الأصل، والثانية تعطيك نسخة جديدة. في التطبيقات الحقيقية هذا يعني أن اختيار الطريقة المناسبة يمكن أن يقيك من أخطاء صعبة في البيانات أو في حالة المتابعة.

const items = ["A", "B"]; // مصفوفة أولية
items.push("C"); // يضيف في النهاية
items.unshift("Z"); // يضيف في البداية
items.pop(); // يحذف آخر عنصر ويعيده
items.shift(); // يحذف أول عنصر ويعيده
items.splice(1, 1, "X", "Y"); // يستبدل أو يضيف في وسط المصفوفة

console.log(items); // مثال النتيجة: ['Z', 'X', 'Y']
const players = ["Ali", "Sami", "Huda"];
players.splice(1, 0, "Omar"); // إدراج بدون حذف
players.splice(2, 1); // حذف عنصر واحد من مكان محدد
console.log(players); // النتيجة النهائية تعكس التعديلات
10

المتغيرات المدمرة وغير المدمرة mutating vs non-mutating methods

بعض الطرق تعدّل الأصل مباشرة، وبعضها يخرج نسخة جديدة. هذا فرق مهم في التطبيقات الحقيقية، لأن التعديل المباشر قد يسبب أخطاء في البيانات. مثلاً إذا كنت تحتفظ بنسخة من قائمة الطلبات وتريد أن تعرف ما إذا كان المستخدم قد عدل قائمة أخرى، فالمتغيرات التي تشير إلى نفس المرجع ستتقاسم نفس التغييرات، بينما النسخ الجديدة تبقى مستقلة.

الطريقة يعدل الأصل؟ النوع
push, pop, shift, unshift نعم Mutating
slice, concat, map, filter لا Non-mutating
sort, reverse, fill نعم Mutating
const numbers = [1, 2, 3];
const copy = numbers.slice(); // نسخة جديدة
const doubled = numbers.map(n => n * 2); // مصفوفة جديدة

numbers.push(4); // يغير الأصل
console.log(numbers); // [1, 2, 3, 4]
console.log(copy); // [1, 2, 3]
11

البحث والاختبارات includes, indexOf, find, findIndex, filter

🔍
تخيل أن عندك قائمة بأسماء الطلاب وتريد البحث فيها. لكن السؤال الذي تطرحه يختلف من مرة لأخرى: "هل اسم معين موجود؟" أم "في أي مكان الاسم؟" أم "أول طالب عمره أكبر من 18؟" كل سؤال مختلف يحتاج طريقة بحث مختلفة.

البحث في المصفوفة واحدة من أكثر المهام شيوعاً في البرمجة. عندما تريد البحث، السؤال الحقيقي هو: ما الذي تريد بالضبط؟ هل تريد معرفة إذا كانت قيمة موجودة أم لا؟ أم تريد معرفة في أي موضع الموجودة؟ أم تريد العنصر نفسه؟ أم تريد كل العناصر التي تحقق شرطاً معيناً؟

🎯 دليل اختيار الطريقة الصحيحة

إذا كنت تريد...

  • معرفة: "هل القيمة موجودة؟" → استخدم includes() لأنها ترجع true أو false
  • معرفة: "في أي موضع أول ظهور؟" → استخدم indexOf() لأنها ترجع الموضع أو -1
  • معرفة: "في أي موضع آخر ظهور؟" → استخدم lastIndexOf() لأنها تبحث من النهاية
  • البحث عن عنصر بشرط معقد والحصول على العنصر نفسه؟ → استخدم find()
  • البحث عن عنصر بشرط معقد والحصول على الموضع؟ → استخدم findIndex()
  • إيجاد كل العناصر التي تحقق الشرط؟ → استخدم filter()

📋 جدول المقارنة

الطريقة ماذا تبحث عن؟ ماذا تعيد؟ إذا لم تجد؟
includes() هل موجود؟ true / false false
indexOf() أول موضع رقم الموضع -1
lastIndexOf() آخر موضع رقم الموضع -1
find() أول عنصر بشرط العنصر نفسه undefined
findIndex() أول موضع بشرط الموضع -1
filter() كل العناصر بشرط مصفوفة جديدة []

1️⃣ includes() - هل موجود أم لا؟

استخدمها عندما تريد معرفة بسيطة جداً: "هل هذا الشيء موجود في القائمة؟" تعيد مباشرة true أو false، لا تحتاج تفاصيل أخرى.

const names = ["Ali", "Huda", "Omar"];

console.log(names.includes("Huda")); // true
console.log(names.includes("Sara")); // false

2️⃣ indexOf() و lastIndexOf() - في أي موضع؟

عندما تريد معرفة الموضع أو الرقم الذي يقع فيه العنصر. indexOf() تجد أول ظهور، وlastIndexOf() تجد آخر ظهور. مفيدة عندما تحتاج إلى حذف أو تعديل عنصر في موضع محدد.

const fruits = ["Apple", "Banana", "Apple", "Orange"];

// البحث عن أول ظهور للـ Apple
console.log(fruits.indexOf("Apple")); // 0

// البحث عن آخر ظهور للـ Apple
console.log(fruits.lastIndexOf("Apple")); // 2

// إذا لم توجد تعيد -1
console.log(fruits.indexOf("Grape")); // -1

3️⃣ find() و findIndex() - البحث بشرط معقد

عندما تبحث عن عنصر ولكن الشرط معقد أكثر من مجرد "هل هذا القيمة موجودة؟" مثل: "ابحث عن أول طالب عمره أكبر من 18" أو "ابحث عن أول منتج سعره أكثر من 1000". find() تعيد العنصر نفسه، وfindIndex() تعيد الموضع.

const products = [
  { name: "Laptop", price: 5000 },
  { name: "Mouse", price: 200 },
  { name: "Phone", price: 3000 }
];

// ابحث عن أول منتج سعره أكثر من 1000
const expensive = products.find((item) => item.price > 1000);
console.log(expensive); // { name: "Laptop", price: 5000 }

// ابحث عن الموضع بنفس الشرط
const idx = products.findIndex((item) => item.price > 1000);
console.log(idx); // 0

4️⃣ filter() - كل العناصر التي تحقق الشرط

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

const products = [
  { name: "Laptop", price: 5000 },
  { name: "Mouse", price: 200 },
  { name: "Phone", price: 3000 }
];

// find: أول منتج غالي فقط
const firstExpensive = products.find(p => p.price > 1000);
console.log(firstExpensive); // { name: "Laptop", price: 5000 }

// filter: كل المنتجات الغالية
const allExpensive = products.filter(p => p.price > 1000);
console.log(allExpensive); // [ { name: "Laptop", ... }, { name: "Phone", ... } ]
12

مقدمة: ما هي الدوال Callback؟ أساس فهم معظم طرق المصفوفات

📞
تخيل أنت زعيم فريق وتقول لفريقك: "اذهبوا ومرّروا على كل الطلاب وسجلوا أسماءهم." "ومرّروا على" هنا تعني أن يعمل الفريق دالة معينة على كل شخص. هذا بالضبط ما تفعل الـ callback.

Callback هي ببساطة دالة صغيرة تعطيها للـ Array method، والـ method هي التي تستدعيها على كل عنصر في المصفوفة. لا تحتاج إلى استدعاء الـ callback بنفسك؛ الـ Array method هي المسؤولة عن استدعاؤها.

مثال بسيط جداً

const numbers = [1, 2, 3];

// هذه دالة بسيطة - callback
const printNumber = (num) => {
  console.log(num); // نعطيها رقم وتطبعه
};

// الآن نعطيها لـ forEach وتستدعيها على كل عنصر
numbers.forEach(printNumber); // forEach هي اللي تستدعي printNumber، ليس أنا

النتيجة:
1
2
3

🎯 الـ Callback تحصل على ثلاث معلومات

عندما تستدعي الـ Array method الـ callback، تعطيها ثلاث معلومات:

  • القيمة (value): القيمة الحالية للعنصر الذي تعمل عليه
  • الفهرس (index): موضع هذا العنصر في المصفوفة
  • المصفوفة كاملة (array): المصفوفة الأصلية
const names = ["Ali", "Huda"];

// الـ callback تحصل على: value, index, array
names.forEach((value, index, array) => {
  console.log(value); // اسم واحد
  console.log(index); // موضعه
  console.log(array); // المصفوفة كاملة
});

💡 ملحوظة مهمة: لا تحتاج إلى استخدام جميع المعاملات الثلاثة. لو احتجت فقط القيمة، اكتب فقط (value). لو احتجت القيمة والموضع، اكتب (value, index).

13

التكرار والتحويل forEach, map, filter, reduce, some, every

🔄
تخيل أنت معلم وعندك قائمة درجات الطلاب. تريد "تمرير" على كل درجة وعمل شيء معها: أحياناً فقط اطبعها (forEach)، أحياناً حوّلها لنسبة (map)، أحياناً اختر الناجحين فقط (filter)، وأحياناً اجمع كل الدرجات في متوسط (reduce).

التكرار هو قلب المصفوفة الحي: نريد أن نمر على كل عنصر ونفعل شيئاً معه. بعض الطرق مثل forEach() لا تعيد مصفوفة جديدة، بل تكرر فقط لأغراض التأثير الجانبي. أما map() و filter() و reduce() فهي طرق تحويل وتجميع، وتعود بقيمة جديدة مفيدة. هذا التمييز مهم لأن كثيراً من الأخطاء تنشأ عندما نستخدم دالة تؤثر على الأصل بدل إنشاء نسخة جديدة.

🎯 دليل الاختيار: أي طريقة أستخدم؟

إذا كنت تريد...

  • تنفيذ عملية على كل عنصر (طباعة، حفظ) بدون نسخة جديدة؟forEach()
  • تحويل كل عنصر إلى قيمة جديدة وإرجاع مصفوفة جديدة؟map()
  • اختيار فقط العناصر التي تحقق شرطاً؟filter()
  • تجميع كل العناصر في قيمة واحدة (مجموع، متوسط، كائن)؟reduce()
  • التحقق: هل يوجد عنصر واحد يحقق الشرط؟some()
  • التحقق: هل كل العناصر تحقق الشرط؟every()

📋 جدول المقارنة الكامل

الطريقة الهدف ماذا تعيد؟ متى تستخدم؟
forEach() تنفيذ عملية على كل عنصر undefined طباعة، تحديث بيانات، عمليات جانبية
map() تحويل كل عنصر مصفوفة جديدة (نفس الطول) تحويل الأرقام، تغيير الصيغة، استخراج جزء
filter() اختيار عناصر بشرط مصفوفة جديدة (قد تكون أصغر) اختيار الناجحين، المنتجات الغالية، الفعالة
reduce() تجميع في قيمة واحدة أي قيمة (رقم، كائن، نص) مجموع، متوسط، تجميع، عد، تجميع بحسب النوع
some() هل يوجد عنصر واحد يحقق الشرط؟ true / false التحقق السريع (توقف عند أول true)
every() هل كل العناصر تحقق الشرط؟ true / false التحقق من الشروط (توقف عند أول false)

1️⃣ forEach() - تنفيذ عملية على كل عنصر

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

const students = ["Ali", "Huda", "Omar"];

// forEach تمر على كل اسم وتطبعه
students.forEach((name, index) => {
  console.log("\u0627\u0644\u0637\u0627\u0644\u0628 \u0631\u0642\u0645 " + (index + 1) + ": " + name); // طباعة بسيطة
});

const result = students.forEach(x => console.log(x));
console.log(result); // undefined - لا تعيد شيء

2️⃣ map() - تحويل كل عنصر إلى قيمة جديدة

map() تحول كل عنصر إلى شيء جديد وترجع مصفوفة جديدة بنفس الطول. الفرق الأساسي: map ترجع نسخة جديدة، forEach لا ترجع شيء.

const numbers = [1, 2, 3];

// map تحول كل رقم إلى ضعفه
const doubled = numbers.map(num => num * 2); // تحويل حقيقي
console.log(doubled); // [2, 4, 6]
console.log(numbers); // [1, 2, 3] - الأصل لم يتغير

3️⃣ filter() - اختيار العناصر التي تحقق شرطاً

filter() تختار فقط العناصر التي تحقق الشرط وترجع مصفوفة جديدة. إذا لم تحقق أي عنصر الشرط، تعيد مصفوفة فارغة [].

const scores = [45, 78, 92, 55, 88];

// filter تختار فقط الدرجات العالية (أكبر من 70)
const passed = scores.filter(score => score > 70); // فقط [78, 92, 88]
console.log(passed); // [78, 92, 88]

// filter تختار الكل الذي لم يحقق الشرط وترجع مصفوفة فارغة
const excellent = scores.filter(score => score > 95); // لا أحد
console.log(excellent); // []

4️⃣ some() و every() - التحقق من الشروط

some(): تتحقق "هل يوجد عنصر واحد على الأقل يحقق الشرط؟" وترجع true أو false. توقف البحث فور إيجاد أول عنصر يحقق (كفاءة!).
every(): تتحقق "هل كل العناصر تحقق الشرط؟" وترجع true أو false. توقف البحث فور إيجاد عنصر لا يحقق.

const ages = [16, 19, 22, 18];

// some: هل يوجد شخص بالغ (أكثر من 18)؟
const hasAdult = ages.some(age => age > 18); // true
console.log(hasAdult); // true

// every: هل الجميع بالغين؟
const allAdults = ages.every(age => age > 18); // false (16 ليس بالغاً)
console.log(allAdults); // false
14

reduce() وتجميع البيانات تحويل مصفوفة كاملة إلى قيمة واحدة

🛒
تخيل أنك تتسوق وتجمع الفواتير. في البداية المجموع = 0، ثم تشتري تفاحة = 10، فيصير المجموع 10. ثم موز = 15، فيصير المجموع 25. وهكذا حتى النهاية. هذا بالضبط ما تفعل reduce()! تبدأ بقيمة أولية وتضيف إليها كل عنصر حتى تصل لقيمة نهائية واحدة.

reduce() هي واحدة من أقوى وأكثر الطرق إرباكاً في JavaScript. الفكرة: لديك قائمة وتريد "طيّها" إلى قيمة واحدة. قد تريد مجموع، متوسط، كائن تجميعي، أو أي قيمة أخرى.

كيف تعمل؟ تبدأ بـ accumulator (قيمة أولية)، ثم تمر على كل عنصر وتضيفه أو تعدّله أو تستخدمه بطريقة ما، وترجع القيمة النهائية.

📊 الخطوات خطوة بخطوة

مثال بسيط: جمع الأرقام [1, 2, 3, 4]

رقم الخطوة accumulator قبل العنصر الحالي العملية accumulator بعد
البداية 0 (القيمة الأولية) - - 0
الخطوة 1 0 1 0 + 1 1
الخطوة 2 1 2 1 + 2 3
الخطوة 3 3 3 3 + 3 6
الخطوة 4 6 4 6 + 4 10 ✅ (النتيجة النهائية)

🎯 معاملات reduce()

الـ callback داخل reduce تحصل على 4 معاملات (لكن غالباً تستخدم الاثنين الأولين):

  • accumulator: القيمة المتجمعة حتى الآن (البداية = القيمة الأولية)
  • currentValue: العنصر الحالي الذي تعمل عليه
  • index (اختياري): موضع العنصر الحالي
  • array (اختياري): المصفوفة الأصلية
// مثال 1: جمع الأرقام (من الأمثلة الشائعة)
const numbers = [1, 2, 3, 4];
const sum = numbers.reduce((acc, num) => {
  return acc + num; // إضافة كل رقم إلى accumulator
}, 0); // 0 هي القيمة الأولية للـ accumulator
console.log(sum); // 10

// مثال 2: عد عناصر بشرط معين
const scores = [45, 78, 92, 55, 88];
const passedCount = scores.reduce((count, score) => {
  return score >= 70 ? count + 1 : count; // إذا درجة >= 70 اجمع 1
}, 0); // نبدأ من 0
console.log(passedCount); // 3 (عدد الدرجات الناجحة)

💡 مثال متقدم: تجميع البيانات بحسب النوع

هذا الاستخدام شائع جداً: أخذ مصفوفة طلبات وعد كم مرة طُلب كل منتج.

const orders = [
  { item: "كتاب", qty: 2 },
  { item: "قلم", qty: 5 },
  { item: "كتاب", qty: 1 },
  { item: "قلم", qty: 3 }
];

// تجميع: كم عدد كل عنصر؟
const summary = orders.reduce((acc, order) => {
  // إذا كان العنصر موجود في accumulator، أضف إلى الكمية
  // وإلا، اجعل الكمية = qty للعنصر الجديد
  acc[order.item] = (acc[order.item] || 0) + order.qty;
  return acc; // أعد الـ accumulator للخطوة التالية
}, {}); // نبدأ بكائن فارغ {} وليس 0

console.log(summary); // { كتاب: 3, قلم: 8 }

⚠️ ملحوظة مهمة: إذا لم تحدد قيمة أولية (مثل 0 أو {})، فإن reduce تستخدم أول عنصر كقيمة أولية، وتبدأ من العنصر الثاني. هذا قد يسبب مشاكل! لذلك دائماً حدد القيمة الأولية.

15

sort() و toSorted() الفرز: الخطر والحل والفرق بين الطريقتين

📑
تخيل أن لديك أوراق مكتوب عليها أرقام وكلمات. تريد ترتيبها: "10" و "2" و "30". إذا ظننت أنها أرقام حقيقية ستصير: 2, 10, 30. لكن إذا ظننتها نصوص (كلمات) ستصير: 10, 2, 30 (مثل ترتيب الحروف الأبجدية!). هذا الفرق يسبب الكثير من الأخطاء!

🚨 المشكلة الشهيرة: الفرز الخاطئ للأرقام

JavaScript بشكل افتراضي يفرز كل شيء كنصوص (strings)! لذلك [10, 2, 30].sort() ترجع [10, 2, 30] وليس [2, 10, 30]! لماذا؟ لأن "10" يأتي قبل "2" كنص (حرف "1" يسبق حرف "2").

const numbers = [10, 2, 30, 5];

// ❌ خطأ شائع: فرز بدون دالة مقارنة
numbers.sort();
console.log(numbers); // [10, 2, 30, 5] - خطأ! (فرز نصي)

const numbers2 = [10, 2, 30, 5];

// ✅ صحيح: فرز برقم مع دالة مقارنة
numbers2.sort((a, b) => a - b); // (a - b) للفرز التصاعدي
console.log(numbers2); // [2, 5, 10, 30] - صحيح!

🎯 كيفية كتابة دالة المقارنة

الـ comparator تحصل على عنصرين (a, b) وترجع:

  • رقم سالب: إذا كان a يجب أن يأتي قبل b
  • 0: إذا كانا متساويان
  • رقم موجب: إذا كان b يجب أن يأتي قبل a

للفرز التصاعدي (من الصغير للكبير): (a, b) => a - b
للفرز التنازلي (من الكبير للصغير): (a, b) => b - a

📌 sort() vs toSorted()

الفرق الأساسي: mutation (تعديل الأصل أم لا)

  • sort(): يعدّل المصفوفة الأصلية مباشرة (mutating)
  • toSorted(): يعيد نسخة جديدة مرتبة، الأصل يبقى كما هو
// مثال 1: sort() تعدّل الأصل
const scores1 = [30, 10, 20];
scores1.sort((a, b) => a - b); // تعديل مباشر
console.log(scores1); // [10, 20, 30] - تغيّر!

// مثال 2: toSorted() تحافظ على الأصل
const scores2 = [30, 10, 20];
const sorted = scores2.toSorted((a, b) => a - b); // نسخة جديدة
console.log(sorted); // [10, 20, 30]
console.log(scores2); // [30, 10, 20] - لم تتغير!

// مثال 3: فرز الكلمات
const names = ["زيد", "علي", "مؤنس"];
const sortedNames = names.toSorted(); // بدون comparator للنصوص عادي
console.log(sortedNames); // ["علي", "مؤنس", "زيد"] - ترتيب أبجدي

💡 مثال واقعي: فرز الطلاب بدرجاتهم

const students = [
  { name: "علي", score: 85 },
  { name: "هدى", score: 92 },
  { name: "عمر", score: 78 }
];

// فرز من الأعلى درجة للأقل
const topScores = students.toSorted((a, b) => b.score - a.score);
// نتيجة: هدى (92) أولاً، ثم علي (85)، ثم عمر (78)
console.log(topScores);

⚠️ تذكير: عند فرز الأرقام، دائماً استخدم دالة مقارنة. عند فرز النصوص والكائنات المعقدة، استخدم also دالة مقارنة مخصصة. اختر بين sort() (تعديل مباشر) و toSorted() (نسخة آمنة) بناءً على احتياجاتك.

16

النسخ والمراجع reference vs shallow copy vs deep copy

📋
تخيل قائمة تسوق على ورقة: - إذا قلت "استخدم نفس الورقة" → كلاكما ترى نفس القائمة وأي تعديل يؤثر على الاثنين - إذا "نسخت الورقة" → لديك ورقة جديدة لكن إذا كانت فيها قائمة متداخلة، لا تزال تشير للقائمة الأصلية - إذا "نسخت كل شيء عميقاً" → لا توجد علاقة بيننا على الإطلاق

🎯 ثلاث حالات مهمة

فهم الفرق يوقيك من أخطاء شديدة جداً:

  • 1. Reference (المرجع): متغيران يشيران للمصفوفة نفسها. أي تغيير ينعكس على الاثنين.
  • 2. Shallow Copy (نسخة سطحية): نسخة جديدة من الطبقة الأولى. لكن العناصر المتداخلة تبقى مرجعية.
  • 3. Deep Copy (نسخة عميقة): نسخة كاملة من كل شيء. لا توجد علاقة بيننا.

📊 جدول الفرق

النوع الطريقة تغيير العنصر الأول تغيير العنصر المتداخل
Reference const b = a ✅ يؤثر على a ✅ يؤثر على a
Shallow const b = [...a] ❌ لا يؤثر ✅ يؤثر على a
Deep const b = structuredClone(a) ❌ لا يؤثر ❌ لا يؤثر
// ❌ الخطأ الشائع: Reference
const original = [1, 2, 3];
const copy = original; // ليست نسخة، بل اسم آخر للمصفوفة نفسها!
copy.push(4);
console.log(original); // [1, 2, 3, 4] - تغيّرت أيضاً!
console.log(copy); // [1, 2, 3, 4]

// ✅ صحيح: Shallow Copy
const original2 = [1, 2, 3];
const copy2 = [...original2]; // نسخة جديدة
copy2.push(4);
console.log(original2); // [1, 2, 3] - لم تتغير!
console.log(copy2); // [1, 2, 3, 4]

⚠️ المشكلة: Shallow Copy والمصفوفات المتداخلة

Shallow copy تعمل جيداً مع الأرقام والنصوص، لكن إذا كان لديك مصفوفة متداخلة أو كائنات متداخلة، التغييرات تؤثر على الأصل أيضاً!

// المشكلة: المصفوفة المتداخلة
const nested = [[1, 2], [3, 4]];
const shallowCopy = [...nested]; // نسخة سطحية

// تغيير العنصر الأول في المصفوفة المتداخلة
shallowCopy[0][0] = 99;

console.log(nested[0][0]); // 99 - تغيّرت أيضاً! المرجع الداخلي لم يُنسخ
console.log(shallowCopy[0][0]); // 99

// الحل: Deep Copy
const deepCopy = structuredClone(nested); // نسخة عميقة كاملة
deepCopy[0][0] = 42;

console.log(nested[0][0]); // 99 - لم تتغير، مستقلة تماماً
console.log(deepCopy[0][0]); // 42

💡 متى تستخدم كل نوع؟
- Reference: عندما تريد تغيير البيانات في مكان واحد وترى التأثير في كل مكان
- Shallow Copy: عندما تتعامل مع بيانات بسيطة (أرقام، نصوص) وتريد نسخة مستقلة
- Deep Copy: عندما تتعامل مع بيانات معقدة (مصفوفات متداخلة، كائنات متشابكة)

17

المصفوفات المتداخلة Nested Arrays - مصفوفات داخل مصفوفات

🏢
تخيل مبنى مدرسة: المبنى فيه طوابق، كل طابق فيه فصول، كل فصل فيه طلاب. عندما تقول "الطالب في الفصل الثاني من الطابق الأول" تحتاج رقمين: رقم الطابق ورقم الفصل. هذا بالضبط كيف تعمل المصفوفات المتداخلة: `matrix[1][2]` معناها "الصف الثاني، العنصر الثالث".

📌 ما هي المصفوفة المتداخلة؟

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

  • الجداول (Matrices): الصفوف والأعمدة
  • الشبكات (Grids): لعبة الشطرنج، الألعاب
  • البيانات المنظمة: جداول بيانات، نتائج الاستبيانات
  • مصفوفات الكائنات (Array of Objects): قائمة مستخدمين، منتجات، طلبات

🎯 كيفية الوصول والتعديل

للوصول إلى عنصر في مصفوفة متداخلة: استخدم فهرسين متتاليين. الفهرس الأول للصف، الثاني للعمود.

// مثال 1: جدول (مصفوفة 2×3)
const matrix = [
  [1, 2, 3],    // الصف الأول (index 0)
  [4, 5, 6],    // الصف الثاني (index 1)
  [7, 8, 9]     // الصف الثالث (index 2)
];

// الوصول إلى العناصر
console.log(matrix[0]); // [1, 2, 3] - الصف الأول كاملاً
console.log(matrix[0][1]); // 2 - العنصر الثاني من الصف الأول
console.log(matrix[2][2]); // 9 - آخر عنصر

// التعديل
matrix[1][1] = 99; // تغيير العنصر الأوسط
console.log(matrix[1]); // [4, 99, 6]

💡 التطبيق الواقعي: مصفوفة من الكائنات

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

// مثال واقعي: قاعدة بيانات مستخدمين
const users = [
  { id: 1, name: "علي", email: "ali@email.com", score: 90 },
  { id: 2, name: "هدى", email: "huda@email.com", score: 95 },
  { id: 3, name: "عمر", email: "omar@email.com", score: 78 }
];

// الوصول إلى البيانات
console.log(users[0]); // الكائن الأول (علي)
console.log(users[0].name); // "علي"
console.log(users[1].score); // 95

// البحث والفلترة
const topStudents = users.filter(user => user.score >= 90);
console.log(topStudents); // [علي، هدى]

// استخراج أسماء فقط
const names = users.map(user => user.name);
console.log(names); // ["علي", "هدى", "عمر"]

⚠️ تذكير مهم: عند التعامل مع مصفوفات متداخلة، تذكر مسألة النسخ (Reference vs Shallow vs Deep). إذا نسخت مصفوفة متداخلة بـ [...] فقط، المصفوفات الداخلية ستبقى مرجعية!

18

التفكيك، والـ spread، والـ rest Destructuring + Spread + Rest Operators

📦
تخيّل أن لديك صندوق فيه ألعاب: كرة، سيارة، دمية، وكتاب.

Destructuring: تخرج كل لعبة إلى يد مختلفة بدل أن تبقيها في الصندوق.
Spread (...): تفتح الصندوق وتنسخ محتوياته لصندوق جديد وتضيف ألعاب أخرى إليه.
Rest (...): تأخذ أول لعبتين وتضع الباقي في كيس منفصل.

Destructuring تجعل العمل مع المصفوفات أسهل: بدل أن تكتب arr[0], arr[1]، تكتب ببساطة const [first, second] = arr وخلاص.

Spread (...) ينشر عناصر المصفوفة لدمجها مع عناصر أخرى أو لنسخها.

Rest (...) يجمع العناصر المتبقية في مصفوفة جديدة بعد أخذ بعضها.

لماذا أحتاجها؟

  • التفكيك (Destructuring): لكي لا تكتب arr[0] و arr[1] كل مرة
  • Spread: لدمج مصفوفات بسهولة وللنسخ الضحلة
  • Rest: لالتقاط العناصر المتبقية دون حساب كم عنصر موجود

متى تستخدم كل واحدة؟

  • لو أريد إخراج عناصر المصفوفة لمتغيرات: → استخدم Destructuring
  • لو أريد نسخ المصفوفة أو دمج مصفوفات: → استخدم Spread (...)
  • لو أريد أخذ بعض العناصر وجعل الباقي في مصفوفة: → استخدم Rest (...)

متى لا تستخدمها؟

  • Destructuring: إذا كانت المصفوفة عميقة جداً (كثير من التداخل) قد تصير معقدة
  • Spread: لا تستخدمه للنسخ العميقة (الهياكل المتداخلة ستبقى مشتركة)
  • Rest: لا تستخدمه في المكان الوسط، فقط في النهاية: ...[middle]...[rest]
الأداة ماذا تفعل؟ الاستخدام
Destructuring إخراج عناصر لمتغيرات منفصلة const [a, b] = arr
Spread (...) نسخ عناصر أو دمج مصفوفات [...arr1, ...arr2]
Rest (...) جمع العناصر المتبقية const [first, ...rest] = arr

مثال 1: Destructuring - إخراج العناصر لمتغيرات

const colors = ["أحمر", "أزرق", "أخضر", "أصفر"];

// الطريقة القديمة (بدون destructuring):
const first = colors[0]; // "أحمر"
const second = colors[1]; // "أزرق"

// الطريقة الجديدة (مع destructuring) - أسهل وأقصر:
const [c1, c2] = colors; // نفس النتيجة بسطر واحد

console.log(c1); // "أحمر"
console.log(c2); // "أزرق"

النتيجة:

c1 = "أحمر" و c2 = "أزرق"

لماذا هذا مهم؟ لأن الكود أقصر وأوضح - بدل 3 أسطر، سطر واحد!

مثال 2: Rest (...) - التقاط العناصر المتبقية

const letters = ["A", "B", "C", "D", "E"];

const [first, second, ...remaining] = letters;
// خذ أول عنصر، ثاني عنصر، والباقي في remaining

console.log(first); // "A"
console.log(second); // "B"
console.log(remaining); // ["C", "D", "E"]

النتيجة: remaining = ["C", "D", "E"]

فائدة هذا: لا تحتاج تعرف كم عنصر في المصفوفة - الثلاثة النقاط (...) تأخذ الباقي تلقائياً!

مثال 3: Spread (...) - دمج المصفوفات

const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];

// بدون spread:
const merged1 = [arr1, arr2]; // [[1,2,3], [4,5,6]] - مصفوفة من مصفوفات!

// مع spread:
const merged2 = [...arr1, ...arr2]; // [1,2,3,4,5,6] - مصفوفة واحدة نظيفة!

console.log(merged2); // [1, 2, 3, 4, 5, 6]

النتيجة: merged2 = [1, 2, 3, 4, 5, 6]

لاحظ الفرق: بدون spread نحصل على [[1,2,3], [4,5,6]] لكن مع spread نحصل على مصفوفة مسطحة!

⚠️ تذكير مهم عن Spread: Spread ينسخ على مستوى ضحل (shallow copy) فقط. إذا كانت المصفوفة تحتوي على كائنات أو مصفوفات متداخلة، تلك الهياكل الداخلية ستبقى مشتركة! اقرأ "References and Copying" إذا أردت النسخ العميقة.

19

حفر المصفوفة والفراغات Sparse Arrays, Holes, delete vs splice

🏚️
تخيّل عمارة فيها 5 شقق مرقمة من 1-5.

Normal Array: كل الشقق مأهولة بالسكان.
Sparse Array: بعض الشقق فارغة (لا أحد بداخلها) لكن أرقامها ما زالت موجودة.

إذا حذفت ساكن الشقة #3 (بدون هدم الشقة)، تبقى الشقة موجودة لكن فارغة! لكن إذا هدمت الشقة كلياً، الشقق بعدها تتحرك ليسد الفراغ.

Sparse Array هي مصفوفة تحتوي على فراغات (holes) - مواقع بدون قيمة. هذا يحدث عادةً عندما تستخدم delete لحذف عنصر.

المشكلة: المصفوفة ما زالت بنفس الطول، لكن هناك فراغات تسبب مشاكل!

الفرق الحيوي: delete vs splice

  • delete arr[i]: يترك الفراغ، الطول ما يتغير ❌ (sparse array)
  • splice(i, 1): يزيل العنصر تماماً، الطول ينخفض ✅ (نظيف)

لماذا هذا مهم؟

  • إذا استخدمت delete قد تحصل على undefined بدل الخطأ
  • الحلقات قد تتوقف أو تسقط الفراغات
  • إعادة الترتيب بتوقع طول مختلف قد تفشل
الطريقة ماذا تفعل؟ الطول النتيجة
delete arr[i] تترك الفراغ ما يتغير Sparse (خطر!)
splice(i, 1) تزيل فعلياً ينخفض نظيفة (آمن!)

مثال 1: المشكلة مع delete

const fruits = ["تفاح", "موز", "برتقال", "عنب"];

console.log("قبل delete:");
console.log(fruits); // ["تفاح", "موز", "برتقال", "عنب"]
console.log("الطول:", fruits.length); // 4

delete fruits[1]; // حذف الموز (لكن الفراغ يبقى!)

console.log("بعد delete:");
console.log(fruits); // ["تفاح", <empty>, "برتقال", "عنب"]
console.log("الطول ما زال:", fruits.length); // ما زال 4! (خطر)
console.log(fruits[1]); // undefined (الفراغ)

المشكلة: الطول لم يتغير! الفراغ ما زال موجود.

⚠️ لماذا هذا مشكلة؟ إذا كتبت حلقة تتوقع طول 4 وتحتاج تملأ الفراغات، قد تفشل!

مثال 2: الحل مع splice

const fruits2 = ["تفاح", "موز", "برتقال", "عنب"];

console.log("قبل splice:");
console.log(fruits2); // ["تفاح", "موز", "برتقال", "عنب"]
console.log("الطول:", fruits2.length); // 4

const removed = fruits2.splice(1, 1); // حذف 1 عنصر من موضع 1

console.log("بعد splice:");
console.log(fruits2); // ["تفاح", "برتقال", "عنب"]
console.log("الطول الآن:", fruits2.length); // 3 (انخفض!)
console.log("العنصر المحذوف:", removed); // ["موز"]

الفرق الواضح: الطول انخفض من 4 إلى 3! المصفوفة نظيفة بدون فراغات.

متى تستخدم كل واحدة؟

  • استخدم splice: دائماً! إذا أردت حذف عنصر فعلياً
  • لا تستخدم delete: إلا إذا كنت تعرف تماماً ماذا تفعل (نادر جداً)

⚠️ القاعدة الذهبية: إذا أردت حذف عنصر من مصفوفة، استخدم splice() وليس delete. delete تترك فراغات تسبب مشاكل غريبة!

20

Array.isArray() والمساواة بين المصفوفات Checking arrays and equality (reference vs value)

🪞
تخيّل لديك نسختان من نفس الصورة.

نفس الصورة (Reference): تشير لنفس الملف في الكمبيوتر - إذا عدّل أحدهما، الاثنين يتغيران.
صورتان مختلفتان (Value): نسختان منفصلتان - تبدوان متشابهتان لكن هما ملفان مختلفان.

JavaScript تسأل: "هل هما نفس الملف؟" وليس "هل يبدوان متشابهان؟"

المشكلة الكبرى في JavaScript: عندما تقول [] === [] النتيجة false!

لماذا؟ لأن كل مصفوفة جديدة لها عنوان مختلف في الذاكرة (reference)، حتى لو بدتا متطابقتان.

المساواة في JavaScript تتحقق من: "هل هما نفس الكائن في الذاكرة؟" وليس: "هل محتوياتهما متطابقة؟"

Array.isArray() - هل هذا مصفوفة؟

typeof تقول "object" للمصفوفات وللكائنات (لا تميز بينهما). لذلك نستخدم Array.isArray() للتحقق: "هل هذا مصفوفة فعلياً؟"

الحالة typeof Array.isArray()
[1, 2, 3] "object" ❌ (غير دقيق) true ✅ (صحيح)
{a: 1} "object" (صحيح لكن عام) false ✅ (مختلف!)
null "object" (خطأ!) false ✅ (صحيح)

مثال 1: المقارنة بالمرجع (Reference)

// مصفوفتان بنفس المحتوى، لكن مراجع مختلفة:
const arr1 = [1, 2, 3]; // عنوان ذاكرة A
const arr2 = [1, 2, 3]; // عنوان ذاكرة B (مختلف!)

console.log(arr1 === arr2); // false (مختلفان)

// لكن إذا أشارت للنفس المرجع:
const arr3 = arr1; // نفس العنوان A
console.log(arr1 === arr3); // true (نفسهما!)

النتيجة:

arr1 === arr2 يعطي false رغم أن المحتوى متطابق!

arr1 === arr3 يعطي true لأنهما يشيران لنفس المكان في الذاكرة.

مثال 2: Checking if something is an Array

const arr = [1, 2, 3];
const obj = {a: 1};

// الطريقة الخاطئة:
console.log(typeof arr); // "object" (لا يميز!)
console.log(typeof obj); // "object" (نفس النتيجة!)

// الطريقة الصحيحة:
console.log(Array.isArray(arr)); // true ✅
console.log(Array.isArray(obj)); // false ✅ (مختلف!)
console.log(Array.isArray(123)); // false
console.log(Array.isArray(null)); // false

لماذا typeof arr غير دقيق؟

typeof تعامل المصفوفات والكائنات بنفس الطريقة لأنها "technically" كائنات في JavaScript. لكن Array.isArray() تتفحص نوع الكائن بدقة.

متى تستخدم كل واحدة؟

  • Array.isArray(): دائماً! عندما تريد التحقق من أن شيء ما مصفوفة
  • ===: فقط عند التحقق من نفس المرجع (هل هما نفس الكائن؟)

⚠️ تذكير مهم: إذا أردت مقارنة محتويات مصفوفتين (ليس المرجع)، لا تستخدم ===. استخدم JSON.stringify() أو حلقة للمقارنة العنصر بالعنصر.

21

الأداء والتعامل العملي مع المصفوفات push/pop vs shift/unshift - لماذا الفرق مهم؟

🚗
تخيّل صف من السيارات في محطة بنزين.

الإضافة في النهاية (push): تضيف سيارة في آخر الصف - سهل وسريع جداً!
الحذف من النهاية (pop): تأخذ آخر سيارة - سهل وسريع جداً!

الإضافة في البداية (unshift): تضيف سيارة في مقدمة الصف - كل السيارات تحتاج تتحرك للخلف (بطيء!)
الحذف من البداية (shift): تأخذ أول سيارة - كل السيارات تحتاج تتقدم للأمام (بطيء!)

لماذا هذا يحدث؟

  • push/pop: تعديل النهاية فقط - سريع جداً ⚡
  • shift/unshift: تحتاج تحريك كل العناصر - بطيء جداً 🐌

عندما تضيف عنصر في البداية، JavaScript تحتاج تغيير فهرس كل عنصر (من index 0 → 1، من 1 → 2، وهكذا...).
كلما كانت المصفوفة أكبر، كلما كان أبطأ!

متى يهم الفرق؟

  • مصفوفة صغيرة (10 عناصر): الفرق بطيء جداً (قد لا تلاحظه)
  • مصفوفة متوسطة (1000 عنصر): قد تلاحظ التباطؤ قليلاً
  • مصفوفة كبيرة (100,000 عنصر): الفرق واضح جداً (ملحوظ)!
العملية المكان السرعة ملاحظة
push() النهاية ⚡ سريع جداً الخيار الأفضل دائماً
pop() النهاية ⚡ سريع جداً الخيار الأفضل دائماً
unshift() البداية 🐌 بطيء تجنبها مع مصفوفات كبيرة
shift() البداية 🐌 بطيء تجنبها مع مصفوفات كبيرة

مثال: المقارنة العملية

// السيناريو: نريد إضافة 5 عناصر جديدة
const list = [1, 2, 3];

// ✅ الطريقة السريعة (في النهاية):
list.push(4); // إضافة في النهاية - سريع!
list.push(5); // إضافة في النهاية - سريع!

const last = list.pop(); // حذف من النهاية - سريع!
console.log(list); // [1, 2, 3, 4]

// ❌ الطريقة البطيئة (في البداية) - تجنب:
list.unshift(0); // إضافة في البداية - بطيء!
list.unshift(-1); // إضافة في البداية - بطيء!

const first = list.shift(); // حذف من البداية - بطيء!
console.log(list); // [-1, 0, 1, 2, 3, 4]

الحل البديل إذا أردت إضافة في البداية:

بدل unshift()، استخدم spread: const newList = [newItem, ...list]
هذا ينسخ المصفوفة كاملة (بدل تحريك العناصر الموجودة).

⚠️ النصيحة الذهبية:
استخدم push/pop دائماً عندما تستطيع.
تجنب shift/unshift خاصة مع مصفوفات كبيرة.

22

الأخطاء الشائعة والفخاخ Common mistakes beginners make

هذه أخطاء شائعة لأن JavaScript مرنة جداً وتسمح بأشياء كثيرة بدون تحذير. لكن المرونة هذه قد تؤدي إلى أخطاء خفية صعب اكتشافها. اعرف هذه الأخطاء الآن وتجنبها في المستقبل!

خطأ 1: استخدام sort() بدون comparator للأرقام

const nums = [10, 2, 30, 5];

// ❌ خطأ - ترتيب نصي:
nums.sort();
console.log(nums); // [10, 2, 30, 5] (لم يتغير! يعتبرها strings)

// ✅ صحيح - مع comparator:
const nums2 = [10, 2, 30, 5];
nums2.sort((a, b) => a - b); // ترتيب رقمي صحيح
console.log(nums2); // [2, 5, 10, 30] ✅

الدرس: دائماً استخدم comparator للأرقام!

خطأ 2: الخلط بين find() و filter()

const students = [
  { name: "علي", score: 85 },
  { name: "هدى", score: 92 },
  { name: "عمر", score: 78 }
];

// ❌ خطأ - find() تعيد عنصر واحد أو undefined:
const result1 = students.find(s => s.score > 80);
console.log(result1); // {name: "علي", score: 85} - عنصر واحد فقط!

// ✅ صحيح - filter() تعيد كل من ينطبق:
const result2 = students.filter(s => s.score > 80);
console.log(result2); // [{علي}, {هدى}] - جميع اللي ينطبق ✅

الدرس: find() = عنصر واحد فقط، filter() = جميع اللي ينطبق

خطأ 3: نسيان return داخل map()

const nums = [1, 2, 3];

// ❌ خطأ - نسيان return:
const doubled1 = nums.map(n => {
  n * 2; // بدون return!
});
console.log(doubled1); // [undefined, undefined, undefined] ❌

// ✅ صحيح - مع return:
const doubled2 = nums.map(n => n * 2); // بدون أقواس = return مباشر
console.log(doubled2); // [2, 4, 6] ✅

الدرس: إذا استخدمت أقواس معقوفة { } لازم تكتب return. أما بدون أقواس فـ JavaScript يفهم أنك تريد return مباشر.

خطأ 4: تعديل المصفوفة الأصلية بدون قصد

const arr = [1, 2, 3];
const copy = arr; // مرجع، ليس نسخ!

// ❌ خطأ - تعديل copy يعدل arr أيضاً:
copy.push(4);
console.log(arr); // [1, 2, 3, 4] - غير مقصود!

// ✅ صحيح - نسخ فعليّ:
const arr2 = [1, 2, 3];
const copy2 = [...arr2]; // نسخة فعليّة!
copy2.push(4);
console.log(arr2); // [1, 2, 3] - لم تتغير ✅

الدرس: const copy = arr تعطيك مرجع وليس نسخ! استخدم [...arr]

خطأ 5: استخدام delete بدل splice

const arr = ["أ", "ب", "ج"];

// ❌ خطأ - delete يترك فراغ:
delete arr[1];
console.log(arr); // ["أ", <empty>, "ج"]
console.log(arr.length); // ما زال 3!

// ✅ صحيح - splice يحذف فعلياً:
const arr2 = ["أ", "ب", "ج"];
arr2.splice(1, 1);
console.log(arr2); // ["أ", "ج"]
console.log(arr2.length); // 2 ✅

الدرس: لا تستخدم delete للحذف من مصفوفة! استخدم splice()

خطأ 6: الاعتقاد أن const يمنع التعديل

const arr = [1, 2, 3]; // const يحمي المرجع، ليس المحتوى!

// ✅ هذا مسموح - تعديل المحتوى:
arr.push(4);
console.log(arr); // [1, 2, 3, 4] - يعمل!

// ❌ هذا غير مسموح - تغيير المرجع:
// arr = [5, 6]; // Error: Assignment to constant variable

الدرس: const يعني "المرجع ثابت" وليس "المحتوى ثابت". يمكنك تعديل العناصر لكن لا تستطيع تغيير المصفوفة الأصلية لمصفوفة جديدة.

⚠️ الخلاصة: تجنب هذه الأخطاء:
1️⃣ sort() بدون comparator للأرقام
2️⃣ الخلط بين find و filter
3️⃣ نسيان return في map
4️⃣ المرجع بدل النسخ
5️⃣ delete بدل splice
6️⃣ الاعتقاد أن const يمنع التعديل

23

المصفوفات في التطبيق الحقيقي shopping cart, grades, users, filters, totals

في التطبيق الحقيقي، المصفوفات لا تتواجد فقط في أمثلة صغيرة؛ بل تُستخدم في عربات التسوق، إدارة الطلاب، الفلاتر، تحليل البيانات، وبناء القوائم. مثال: تريد جمع قيمة كل سلعة في السلة في مبلغ واحد، أو اختيار العناصر التي تتجاوز سعرًا معينًا، أو تحويل قائمة كائنات إلى قائمة أسماء فقط. هذه الأنماط هي جزء أساسي من عمل المطورين في الواقع.

const cart = [
  { name: "Laptop", price: 5000, qty: 1 },
  { name: "Mouse", price: 200, qty: 2 }
];

const total = cart.reduce((sum, item) => sum + (item.price * item.qty), 0);
console.log(total); // 5400

const expensiveItems = cart.filter(item => item.price > 1000);
console.log(expensiveItems.map(item => item.name)); // ['Laptop']