تخيل أن المصفوفة (Array) مثل "خزانة أدراج مرقمة". كل درج له رقم، وتضع فيه ألعابك. المهم أنك تعرف في أي درج وضعت اللعبة لتسترجعها بسهولة.
ما يميزها هو النظام الصارم: اللعبة الأولى تظل في المكان الأول، والثانية في المكان الثاني.
// هذا مثال لمصفوفة بسيطة
// قمنا بإنشاء خزانة اسمها "toys" ووضعنا فيها 3 ألعاب
const toys = ["Car", "Doll", "Ball"];
// طبعاً المصفوفة نفسها تعتبر "كائن" (Object) لكنه ذكي ومرتب
console.log(toys); // سيطبع: ["Car", "Doll", "Ball"]
عند إنشاء مصفوفة، نريد أن نكون واضحين جدًا بشأن المعنى. الطريقة الأولى [ ] تُنشئ مصفوفة حقيقية مع عناصر فعليّة فوراً، بينما new Array(5) يفسّر الرقم كطول فقط وليس كقيمة أولية. هذه اللمسة الصغيرة تغيّر سلوك المصفوفة بالكامل، لذلك من السهل أن نخلط بين الطريقتين إذا لم ننتبه.
الأفضل والأكثر أماناً، كأنك تجمع أدواتك في حقيبة بسرعة.
// استخدمنا const لإنشاء ثابت اسمه myFavColors
// القوسين [ ] يخبران الكمبيوتر: "اصنع لي مصفوفة جديدة"
const myFavColors = ["Red", "Blue", "Green"];
// الآن لدينا مصفوفة جاهزة ومليئة بالألوان
console.log(myFavColors); // Red Blue Green
تحتاج حذر! الكمبيوتر يلعب بنا لعبة عندما نمرر رقماً واحداً.
const riskyArray = new Array(5);
// [empty × 5] (خمس خانات فارغة)
const correctArray = new Array();
correctArray[0] = 5; // النتيجة: [5]
إذا طلبت فهرساً غير موجود، لا يحدث خطأ في معظم الحالات. ستُرجع 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
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]
أداة "غبية" تقول عن المصفوفة "object" فقط.
أداة "ذكية" تعرف المصفوفة بدقة مطلقة.
عند التعامل مع القيم من مصادر مختلفة، نحتاج أحياناً إلى معرفة: "هل هذا فعلاً مصفوفة؟" لأن 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 ✅
تحويل النصوص أو الـ NodeList إلى مصفوفة حقيقية.
let chars = Array.from("Hello"); // ['H','e','l','l','o']
هناك أكثر من طريقة لإنشاء مصفوفة، وكل طريقة لها سلوك مختلف. هذا مهم لأن بعض الطرق تترك "فراغات" في المصفوفة، بينما أخرى تنشئ عناصر فعليّة. فالـ Array literal واضح ومباشر، أما new Array() فله سلوك خاص عند تمرير رقم واحد فقط، ويحمل في هذا الحالة معنى الطول وليس معنى العنصر. أما Array.from() فمفيدة عندما نريد تحويل شيء قابل للتكرار مثل النص أو قائمة DOM إلى مصفوفة حقيقية.
const numbers = [1, 2, 3]; // إنشاء مصفوفة مباشرة مع القيم
const empty = []; // مصفوفة فارغة
const mix = [1, "Ali", true, { name: "Eslam" }]; // أنواع مختلفة داخل نفس المصفوفة
const a = new Array(3); // ينتج ثلاث فراغات، لا ثلاث عناصر
console.log(a.length); // 3
console.log(a[0]); // undefined
const b = new Array(1, 2, 3); // هنا يتم إنشاء 3 عناصر فعليّة
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]
هذا مفيد جداً عندما نريد الوصول إلى آخر عنصر أو الثاني من آخر بدون حساب يدوي. كما أنه يمنع الأخطاء الشائعة عند التعامل مع المصفوفات الطويلة أو عند كتابة منطق يعتمد على آخر قيمها. الفهرس السالب لا يعمل مع الوصول العادي 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 لأن الفهرس غير موجود
مفهوم 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 لأن هذا فراغ لا قيمة حقيقية
هذه الطرق تغير المصفوفة الأصلية مباشرة. بعض الطرق تعطيك قيمة مرتجعة مثل العدد الجديد أو العنصر المحذوف، وأخرى تعطيك مصفوفة جديدة. من هنا يأتي فرق أساسي بين 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); // النتيجة النهائية تعكس التعديلات
بعض الطرق تعدّل الأصل مباشرة، وبعضها يخرج نسخة جديدة. هذا فرق مهم في التطبيقات الحقيقية، لأن التعديل المباشر قد يسبب أخطاء في البيانات. مثلاً إذا كنت تحتفظ بنسخة من قائمة الطلبات وتريد أن تعرف ما إذا كان المستخدم قد عدل قائمة أخرى، فالمتغيرات التي تشير إلى نفس المرجع ستتقاسم نفس التغييرات، بينما النسخ الجديدة تبقى مستقلة.
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]
البحث في المصفوفة واحدة من أكثر المهام شيوعاً في البرمجة. عندما تريد البحث، السؤال الحقيقي هو: ما الذي تريد بالضبط؟ هل تريد معرفة إذا كانت قيمة موجودة أم لا؟ أم تريد معرفة في أي موضع الموجودة؟ أم تريد العنصر نفسه؟ أم تريد كل العناصر التي تحقق شرطاً معيناً؟
إذا كنت تريد...
استخدمها عندما تريد معرفة بسيطة جداً: "هل هذا الشيء موجود في القائمة؟" تعيد مباشرة true أو false، لا تحتاج تفاصيل أخرى.
const names = ["Ali", "Huda", "Omar"];
console.log(names.includes("Huda")); // true
console.log(names.includes("Sara")); // false
عندما تريد معرفة الموضع أو الرقم الذي يقع فيه العنصر. 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
عندما تبحث عن عنصر ولكن الشرط معقد أكثر من مجرد "هل هذا القيمة موجودة؟" مثل: "ابحث عن أول طالب عمره أكبر من 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
إذا كنت تريد أول عنصر فقط استخدم 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", ... } ]
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
عندما تستدعي الـ Array method الـ callback، تعطيها ثلاث معلومات:
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).
التكرار هو قلب المصفوفة الحي: نريد أن نمر على كل عنصر ونفعل شيئاً معه. بعض الطرق مثل forEach() لا تعيد مصفوفة جديدة، بل تكرر فقط لأغراض التأثير الجانبي. أما map() و filter() و reduce() فهي طرق تحويل وتجميع، وتعود بقيمة جديدة مفيدة. هذا التمييز مهم لأن كثيراً من الأخطاء تنشأ عندما نستخدم دالة تؤثر على الأصل بدل إنشاء نسخة جديدة.
إذا كنت تريد...
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 - لا تعيد شيء
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] - الأصل لم يتغير
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); // []
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
reduce() هي واحدة من أقوى وأكثر الطرق إرباكاً في JavaScript. الفكرة: لديك قائمة وتريد "طيّها" إلى قيمة واحدة. قد تريد مجموع، متوسط، كائن تجميعي، أو أي قيمة أخرى.
كيف تعمل؟ تبدأ بـ accumulator (قيمة أولية)، ثم تمر على كل عنصر وتضيفه أو تعدّله أو تستخدمه بطريقة ما، وترجع القيمة النهائية.
مثال بسيط: جمع الأرقام [1, 2, 3, 4]
الـ callback داخل reduce تحصل على 4 معاملات (لكن غالباً تستخدم الاثنين الأولين):
// مثال 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 تستخدم أول عنصر كقيمة أولية، وتبدأ من العنصر الثاني. هذا قد يسبب مشاكل! لذلك دائماً حدد القيمة الأولية.
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) => a - b
للفرز التنازلي (من الكبير للصغير):
(a, b) => b - a
الفرق الأساسي: mutation (تعديل الأصل أم لا)
// مثال 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() (نسخة آمنة) بناءً على احتياجاتك.
فهم الفرق يوقيك من أخطاء شديدة جداً:
// ❌ الخطأ الشائع: 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 تعمل جيداً مع الأرقام والنصوص، لكن إذا كان لديك مصفوفة متداخلة أو كائنات متداخلة، التغييرات تؤثر على الأصل أيضاً!
// المشكلة: المصفوفة المتداخلة
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: عندما تتعامل مع بيانات معقدة (مصفوفات
متداخلة، كائنات متشابكة)
هي ببساطة مصفوفة تحتوي على مصفوفات أخرى كعناصر. بدل أن تكون العناصر أرقام أو نصوص، هي مصفوفات نفسها. هذا يسمح لك بتمثيل:
للوصول إلى عنصر في مصفوفة متداخلة: استخدم فهرسين متتاليين. الفهرس الأول للصف، الثاني للعمود.
// مثال 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). إذا نسخت مصفوفة متداخلة
بـ [...] فقط، المصفوفات الداخلية ستبقى مرجعية!
Destructuring تجعل العمل مع المصفوفات أسهل: بدل أن
تكتب arr[0], arr[1]، تكتب ببساطة
const [first, second] = arr وخلاص.
Spread (...) ينشر عناصر المصفوفة لدمجها مع عناصر
أخرى أو لنسخها.
Rest (...) يجمع العناصر المتبقية في مصفوفة جديدة
بعد أخذ بعضها.
لماذا أحتاجها؟
arr[0] و
arr[1] كل مرة
متى تستخدم كل واحدة؟
متى لا تستخدمها؟
...[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" إذا أردت النسخ العميقة.
Sparse Array هي مصفوفة تحتوي على فراغات (holes) -
مواقع بدون قيمة. هذا يحدث عادةً عندما تستخدم
delete لحذف عنصر.
المشكلة: المصفوفة ما زالت بنفس الطول، لكن هناك فراغات تسبب مشاكل!
الفرق الحيوي: delete vs splice
لماذا هذا مهم؟
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.
delete تترك فراغات تسبب مشاكل غريبة!
المشكلة الكبرى في 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() تتفحص نوع الكائن بدقة.
متى تستخدم كل واحدة؟
⚠️ تذكير مهم: إذا أردت مقارنة محتويات مصفوفتين (ليس
المرجع)، لا تستخدم ===. استخدم
JSON.stringify() أو حلقة للمقارنة العنصر بالعنصر.
لماذا هذا يحدث؟
عندما تضيف عنصر في البداية، JavaScript تحتاج تغيير فهرس كل عنصر (من
index 0 → 1، من 1 → 2، وهكذا...).
كلما كانت المصفوفة أكبر، كلما كان أبطأ!
متى يهم الفرق؟
| العملية | المكان | السرعة | ملاحظة |
| 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 خاصة مع مصفوفات كبيرة.
هذه أخطاء شائعة لأن 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 يمنع التعديل
في التطبيق الحقيقي، المصفوفات لا تتواجد فقط في أمثلة صغيرة؛ بل تُستخدم في عربات التسوق، إدارة الطلاب، الفلاتر، تحليل البيانات، وبناء القوائم. مثال: تريد جمع قيمة كل سلعة في السلة في مبلغ واحد، أو اختيار العناصر التي تتجاوز سعرًا معينًا، أو تحويل قائمة كائنات إلى قائمة أسماء فقط. هذه الأنماط هي جزء أساسي من عمل المطورين في الواقع.
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']