أساسيات JavaScript

توابع المصفوفات: push و pop و shift و unshift و splice

45 دقيقة الدرس 14 من 60

فهم التوابع المُعدِّلة مقابل غير المُعدِّلة

قبل الغوص في توابع محددة للمصفوفات، من الضروري فهم الفرق بين التوابع المُعدِّلة و غير المُعدِّلة. التابع المُعدِّل (يسمى أيضا تابع "في المكان") يغير المصفوفة الأصلية مباشرة. بعد استدعاء تابع مُعدِّل، تتغير المصفوفة التي بدأت بها بشكل دائم. التابع غير المُعدِّل ينشئ ويُرجع مصفوفة جديدة دون لمس الأصلية. تبقى الأصلية كما هي تماما.

هذا التمييز مهم جدا في التطبيقات الواقعية. في أطر عمل جافاسكريبت الحديثة مثل React و Vue و Angular، تعديل الحالة مباشرة يمكن أن يسبب أخطاء، وإعادة عرض مفقودة، ومشاكل صعبة التتبع. يفضل العديد من المطورين التوابع غير المُعدِّلة لأنها تجعل الكود أكثر قابلية للتنبؤ -- تعرف دائما أن بياناتك الأصلية آمنة. ومع ذلك، التوابع المُعدِّلة مقبولة تماما عندما تملك البيانات والأداء مهم، مثل داخل دالة محلية تنشئ وتعدل مصفوفتها الخاصة.

في هذا الدرس، سنغطي أهم التوابع المُعدِّلة أولا، ثم ننتقل إلى البدائل غير المُعدِّلة. بنهاية الدرس، ستفهم تماما متى تستخدم كل نهج وكيفية التحويل بينهما.

مثال: التعديل مقابل عدم التغيير بنظرة سريعة

// مُعدِّل: المصفوفة الأصلية تتغير
const fruits = ['apple', 'banana'];
fruits.push('cherry');
console.log(fruits); // ['apple', 'banana', 'cherry'] -- الأصلية تغيرت!

// غير مُعدِّل: مصفوفة جديدة تُرجع، الأصلية لم تُمس
const numbers = [1, 2, 3];
const doubled = numbers.map(n => n * 2);
console.log(numbers); // [1, 2, 3]       -- الأصلية لم تتغير
console.log(doubled); // [2, 4, 6]       -- مصفوفة جديدة

// نمط شائع: إنشاء مصفوفة جديدة بدلا من التعديل
const original = ['a', 'b', 'c'];
const withD = [...original, 'd']; // إضافة غير مُعدِّلة
console.log(original); // ['a', 'b', 'c'] -- آمنة
console.log(withD);    // ['a', 'b', 'c', 'd']

push() -- إضافة عناصر إلى النهاية

تابع push() يضيف عنصرا واحدا أو أكثر إلى نهاية المصفوفة ويُرجع الطول الجديد للمصفوفة. هذا هو التابع الأكثر استخداما لإضافة عناصر إلى مصفوفة. إنه تابع مُعدِّل -- يعدل المصفوفة الأصلية في مكانها.

مثال: استخدام push()

const colors = ['red', 'green'];

// إضافة عنصر واحد
const newLength = colors.push('blue');
console.log(colors);    // ['red', 'green', 'blue']
console.log(newLength); // 3 (push يُرجع الطول الجديد)

// إضافة عناصر متعددة دفعة واحدة
colors.push('yellow', 'purple', 'orange');
console.log(colors);
// ['red', 'green', 'blue', 'yellow', 'purple', 'orange']

// دفع عناصر من مصفوفة أخرى باستخدام الانتشار
const moreColors = ['pink', 'cyan'];
colors.push(...moreColors);
console.log(colors);
// ['red', 'green', 'blue', 'yellow', 'purple', 'orange', 'pink', 'cyan']

// push يُرجع الطول الجديد، وليس المصفوفة
const items = [];
console.log(items.push('first'));  // 1
console.log(items.push('second')); // 2
console.log(items.push('third'));  // 3
console.log(items); // ['first', 'second', 'third']
ملاحظة: push() يُرجع الطول الجديد للمصفوفة، وليس المصفوفة نفسها. هذا مصدر شائع للالتباس. إذا كنت بحاجة للمصفوفة، أشر إليها مباشرة بدلا من استخدام القيمة المُرجعة من push().

pop() -- إزالة عناصر من النهاية

تابع pop() يزيل العنصر الأخير من المصفوفة ويُرجع ذلك العنصر المُزال. إذا كانت المصفوفة فارغة، pop() يُرجع undefined. مثل push()، إنه تابع مُعدِّل. معا، push() و pop() يسمحان لك باستخدام مصفوفة كهيكل بيانات مكدس (آخر دخول، أول خروج / LIFO).

مثال: استخدام pop()

const stack = ['page1', 'page2', 'page3', 'page4'];

// إزالة والحصول على العنصر الأخير
const lastPage = stack.pop();
console.log(lastPage); // 'page4'
console.log(stack);    // ['page1', 'page2', 'page3']

// pop مرة أخرى
const anotherPage = stack.pop();
console.log(anotherPage); // 'page3'
console.log(stack);        // ['page1', 'page2']

// pop من مصفوفة فارغة
const emptyArr = [];
const result = emptyArr.pop();
console.log(result);   // undefined
console.log(emptyArr); // []

// استخدام push و pop كمكدس (LIFO)
const undoStack = [];
undoStack.push('كتابة A');
undoStack.push('كتابة B');
undoStack.push('كتابة C');
console.log(undoStack); // ['كتابة A', 'كتابة B', 'كتابة C']

// التراجع عن الإجراءات بترتيب عكسي
console.log(undoStack.pop()); // 'كتابة C' (آخر إجراء يُتراجع عنه أولا)
console.log(undoStack.pop()); // 'كتابة B'
console.log(undoStack);       // ['كتابة A']

unshift() -- إضافة عناصر إلى البداية

تابع unshift() يضيف عنصرا واحدا أو أكثر إلى بداية المصفوفة ويُرجع الطول الجديد. جميع العناصر الموجودة تُزاح إلى فهارس أعلى لتوفير مكان. هذا تابع مُعدِّل. انتبه أن unshift() بشكل عام أبطأ من push() لأن كل عنصر موجود يجب إعادة فهرسته.

مثال: استخدام unshift()

const queue = ['second', 'third'];

// إضافة عنصر واحد إلى البداية
const newLen = queue.unshift('first');
console.log(queue);   // ['first', 'second', 'third']
console.log(newLen);  // 3

// إضافة عناصر متعددة إلى البداية
queue.unshift('zero-a', 'zero-b');
console.log(queue);
// ['zero-a', 'zero-b', 'first', 'second', 'third']

// ترتيب الوسائط المتعددة يُحفظ
const nums = [4, 5];
nums.unshift(1, 2, 3);
console.log(nums); // [1, 2, 3, 4, 5]
// ملاحظة: 1، 2، 3 تُدرج كمجموعة، وليس واحدا تلو الآخر

// قارن: الإدراج واحدا تلو الآخر يعكس الترتيب
const nums2 = [4, 5];
nums2.unshift(3);
nums2.unshift(2);
nums2.unshift(1);
console.log(nums2); // [1, 2, 3, 4, 5] -- نفس النتيجة هنا، لكن التوقيت يختلف
تحذير أداء: unshift() له تعقيد زمني O(n) لأن كل عنصر موجود يجب نقله إلى فهرس جديد. للمصفوفات الكبيرة ذات آلاف العناصر، استدعاءات unshift() المتكررة يمكن أن تسبب مشاكل أداء ملحوظة. إذا كنت بحاجة لإضافة عناصر إلى البداية بشكل متكرر، فكر في استخدام هيكل بيانات مختلف أو عكس منطقك لاستخدام push() بدلا من ذلك.

shift() -- إزالة عناصر من البداية

تابع shift() يزيل العنصر الأول من المصفوفة ويُرجع ذلك العنصر المُزال. جميع العناصر المتبقية تُزاح إلى فهارس أدنى. مثل unshift()، له تعقيد O(n). معا، push() و shift() يسمحان لك باستخدام مصفوفة كهيكل بيانات طابور (أول دخول، أول خروج / FIFO).

مثال: استخدام shift()

const tasks = ['البريد', 'الاجتماع', 'مراجعة الكود', 'النشر'];

// إزالة والحصول على العنصر الأول
const firstTask = tasks.shift();
console.log(firstTask); // 'البريد'
console.log(tasks);     // ['الاجتماع', 'مراجعة الكود', 'النشر']

// shift مرة أخرى
const nextTask = tasks.shift();
console.log(nextTask); // 'الاجتماع'
console.log(tasks);    // ['مراجعة الكود', 'النشر']

// shift من مصفوفة فارغة
const empty = [];
console.log(empty.shift()); // undefined

// استخدام push و shift كطابور (FIFO)
const printQueue = [];
printQueue.push('المستند أ');
printQueue.push('المستند ب');
printQueue.push('المستند ج');
console.log(printQueue); // ['المستند أ', 'المستند ب', 'المستند ج']

// المعالجة بالترتيب (أول دخول، أول خروج)
console.log(printQueue.shift()); // 'المستند أ' (أول ما أُضيف، أول ما يُعالج)
console.log(printQueue.shift()); // 'المستند ب'
console.log(printQueue);         // ['المستند ج']
نصيحة: تذكر الأزواج: push/pop يعملان في نهاية المصفوفة، unshift/shift يعملان في بداية المصفوفة. التوابع التي تضيف عناصر (push، unshift) تُرجع الطول الجديد. التوابع التي تزيل عناصر (pop، shift) تُرجع العنصر المُزال.

splice() -- السكين السويسرية

تابع splice() هو أقوى وأكثر توابع المصفوفات المُعدِّلة تنوعا. يمكنه إضافة و إزالة و استبدال عناصر في أي موقع في المصفوفة، كل ذلك في استدعاء واحد. صيغته هي: array.splice(startIndex, deleteCount, ...itemsToInsert). يُرجع مصفوفة من العناصر المُزالة (فارغة إذا لم يُزل شيء).

إزالة العناصر بـ splice()

مثال: إزالة العناصر

const months = ['يناير', 'فبراير', 'مارس', 'أبريل', 'مايو', 'يونيو'];

// إزالة عنصر واحد من الفهرس 2
const removed = months.splice(2, 1);
console.log(removed); // ['مارس']
console.log(months);  // ['يناير', 'فبراير', 'أبريل', 'مايو', 'يونيو']

// إزالة عنصرين بدءا من الفهرس 3
const removedTwo = months.splice(3, 2);
console.log(removedTwo); // ['مايو', 'يونيو']
console.log(months);     // ['يناير', 'فبراير', 'أبريل']

// إزالة كل شيء من الفهرس 1 فصاعدا (حذف deleteCount)
const letters = ['a', 'b', 'c', 'd', 'e'];
const removedAll = letters.splice(1);
console.log(removedAll); // ['b', 'c', 'd', 'e']
console.log(letters);    // ['a']

// عدم إزالة شيء (deleteCount = 0)
const items = ['x', 'y', 'z'];
const removedNone = items.splice(1, 0);
console.log(removedNone); // [] (مصفوفة فارغة -- لم يُزل شيء)
console.log(items);       // ['x', 'y', 'z'] (بدون تغيير)

// استخدام فهرس سلبي (يعد من النهاية)
const data = [10, 20, 30, 40, 50];
data.splice(-2, 1); // إزالة عنصر واحد بدءا من ما قبل الأخير
console.log(data); // [10, 20, 30, 50]

إضافة العناصر بـ splice()

مثال: إدراج العناصر

const languages = ['JavaScript', 'Python', 'Java'];

// إدراج 'TypeScript' في الفهرس 1 (حذف 0 عنصر)
languages.splice(1, 0, 'TypeScript');
console.log(languages);
// ['JavaScript', 'TypeScript', 'Python', 'Java']

// إدراج عناصر متعددة في الفهرس 3
languages.splice(3, 0, 'Go', 'Rust');
console.log(languages);
// ['JavaScript', 'TypeScript', 'Python', 'Go', 'Rust', 'Java']

// إدراج في البداية (الفهرس 0)
languages.splice(0, 0, 'HTML');
console.log(languages);
// ['HTML', 'JavaScript', 'TypeScript', 'Python', 'Go', 'Rust', 'Java']

// إدراج في النهاية (باستخدام length كفهرس)
languages.splice(languages.length, 0, 'C++');
console.log(languages);
// ['HTML', 'JavaScript', 'TypeScript', 'Python', 'Go', 'Rust', 'Java', 'C++']

استبدال العناصر بـ splice()

مثال: استبدال العناصر

const team = ['Alice', 'Bob', 'Charlie', 'Diana', 'Eve'];

// استبدال عنصر واحد في الفهرس 2
const replaced = team.splice(2, 1, 'Carlos');
console.log(replaced); // ['Charlie'] (العنصر المُزال)
console.log(team);     // ['Alice', 'Bob', 'Carlos', 'Diana', 'Eve']

// استبدال عنصرين بـ 3 جدد (المصفوفة تكبر)
team.splice(1, 2, 'Bilal', 'Carla', 'Carmen');
console.log(team);
// ['Alice', 'Bilal', 'Carla', 'Carmen', 'Diana', 'Eve']

// استبدال 3 عناصر بعنصر واحد (المصفوفة تصغر)
team.splice(1, 3, 'Brian');
console.log(team); // ['Alice', 'Brian', 'Diana', 'Eve']

// استبدال العنصر الأخير
team.splice(-1, 1, 'Emily');
console.log(team); // ['Alice', 'Brian', 'Diana', 'Emily']
ملاحظة: تابع splice() يُرجع دائما مصفوفة من العناصر المُزالة، حتى لو أُزيل عنصر واحد فقط. إذا لم تُزل أي عناصر، يُرجع مصفوفة فارغة []. هذا يجعل استخدام القيمة المُرجعة آمنا دائما دون الحاجة للتحقق.

reverse() -- عكس ترتيب المصفوفة

تابع reverse() يعكس ترتيب العناصر في مصفوفة في مكانها ويُرجع المصفوفة المعكوسة (نفس المرجع). هذا تابع مُعدِّل. إذا كنت بحاجة لنسخة معكوسة دون تعديل الأصل، استخدم toReversed() (ES2023) أو الانتشار ثم العكس.

مثال: استخدام reverse()

const sequence = [1, 2, 3, 4, 5];

// العكس في المكان
const reversed = sequence.reverse();
console.log(sequence); // [5, 4, 3, 2, 1]
console.log(reversed); // [5, 4, 3, 2, 1]
console.log(reversed === sequence); // true (نفس المرجع!)

// عكس سلسلة نصية (باستخدام تحويل المصفوفة)
const str = 'Hello World';
const reversedStr = [...str].reverse().join('');
console.log(reversedStr); // 'dlroW olleH'

// عكس غير مُعدِّل باستخدام الانتشار
const original = ['a', 'b', 'c', 'd'];
const reversedCopy = [...original].reverse();
console.log(original);     // ['a', 'b', 'c', 'd'] -- لم تتغير
console.log(reversedCopy); // ['d', 'c', 'b', 'a']

// ES2023: toReversed() (غير مُعدِّل)
const nums = [10, 20, 30, 40];
const numsReversed = nums.toReversed();
console.log(nums);         // [10, 20, 30, 40] -- لم تتغير
console.log(numsReversed); // [40, 30, 20, 10]

sort() -- ترتيب عناصر المصفوفة

تابع sort() يرتب عناصر المصفوفة في مكانها ويُرجع المصفوفة المُرتبة. افتراضيا، sort() يحول العناصر إلى سلاسل نصية ويرتبها بترتيب معجمي (أبجدي/يونيكود). هذا يعمل جيدا مع السلاسل النصية لكنه ينتج نتائج مفاجئة مع الأرقام. للترتيب بشكل صحيح، يجب أن توفر دالة مقارنة.

مثال: سلوك الترتيب الافتراضي

// ترتيب السلاسل النصية يعمل كما هو متوقع
const fruits = ['banana', 'apple', 'cherry', 'date'];
fruits.sort();
console.log(fruits); // ['apple', 'banana', 'cherry', 'date']

// ترتيب الأرقام بدون دالة مقارنة -- خاطئ!
const numbers = [10, 5, 100, 25, 1];
numbers.sort();
console.log(numbers); // [1, 10, 100, 25, 5]  -- غير صحيح!
// الأرقام تُحول إلى سلاسل نصية: '1' < '10' < '100' < '25' < '5'

// الأحرف الكبيرة والصغيرة تُرتب بشكل مختلف
const mixed = ['banana', 'Apple', 'cherry', 'avocado'];
mixed.sort();
console.log(mixed); // ['Apple', 'avocado', 'banana', 'cherry']
// الأحرف الكبيرة تأتي قبل الصغيرة في يونيكود
تحذير حاسم: لا تستخدم أبدا sort() بدون دالة مقارنة عند ترتيب الأرقام. الترتيب الافتراضي المبني على السلاسل النصية سيعطيك نتائج خاطئة. مرر دائما دالة مقارنة: arr.sort((a, b) => a - b) للترتيب التصاعدي، arr.sort((a, b) => b - a) للترتيب التنازلي.

مثال: الترتيب مع دوال المقارنة

// ترتيب رقمي تصاعدي
const scores = [85, 92, 78, 100, 65, 88];
scores.sort((a, b) => a - b);
console.log(scores); // [65, 78, 85, 88, 92, 100]

// ترتيب رقمي تنازلي
scores.sort((a, b) => b - a);
console.log(scores); // [100, 92, 88, 85, 78, 65]

// كيف تعمل دالة المقارنة:
// إذا (a - b) < 0: a تأتي قبل b
// إذا (a - b) > 0: b تأتي قبل a
// إذا (a - b) === 0: الترتيب لا يتغير

// ترتيب سلاسل نصية بتجاهل حالة الأحرف
const names = ['charlie', 'Alice', 'bob', 'Diana'];
names.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
console.log(names); // ['Alice', 'bob', 'charlie', 'Diana']

// ترتيب الكائنات حسب خاصية
const students = [
    { name: 'Ali', grade: 88 },
    { name: 'Sara', grade: 95 },
    { name: 'Omar', grade: 72 },
    { name: 'Layla', grade: 91 }
];

// ترتيب حسب الدرجة (تصاعدي)
students.sort((a, b) => a.grade - b.grade);
console.log(students.map(s => `${s.name}: ${s.grade}`));
// ['Omar: 72', 'Ali: 88', 'Layla: 91', 'Sara: 95']

// ترتيب حسب الاسم (أبجدي)
students.sort((a, b) => a.name.localeCompare(b.name));
console.log(students.map(s => s.name));
// ['Ali', 'Layla', 'Omar', 'Sara']

// ترتيب متعدد المعايير: حسب الدرجة تنازليا، ثم حسب الاسم تصاعديا
const classmates = [
    { name: 'Ali', grade: 88 },
    { name: 'Sara', grade: 88 },
    { name: 'Omar', grade: 95 },
    { name: 'Layla', grade: 88 }
];
classmates.sort((a, b) => {
    if (b.grade !== a.grade) return b.grade - a.grade;
    return a.name.localeCompare(b.name);
});
console.log(classmates.map(s => `${s.name}: ${s.grade}`));
// ['Omar: 95', 'Ali: 88', 'Layla: 88', 'Sara: 88']
نصيحة: ES2023 قدمت toSorted()، نسخة غير مُعدِّلة من sort(). استخدم const sorted = arr.toSorted((a, b) => a - b) للحصول على نسخة مُرتبة دون تعديل المصفوفة الأصلية.

fill() -- ملء عناصر المصفوفة

تابع fill() يملأ كل (أو جزء من) عناصر المصفوفة بقيمة ثابتة. صيغته هي array.fill(value, startIndex, endIndex). فهرس البداية شامل وفهرس النهاية حصري. هذا تابع مُعدِّل مفيد لتهيئة المصفوفات أو إعادة تعيين أقسام من البيانات.

مثال: استخدام fill()

// ملء مصفوفة كاملة
const zeros = new Array(5).fill(0);
console.log(zeros); // [0, 0, 0, 0, 0]

// ملء بقيمة محددة
const stars = new Array(3).fill('*');
console.log(stars); // ['*', '*', '*']

// ملء جزئي (بدءا من الفهرس 2، انتهاء قبل الفهرس 4)
const arr = [1, 2, 3, 4, 5];
arr.fill(0, 2, 4);
console.log(arr); // [1, 2, 0, 0, 5]

// ملء من فهرس إلى النهاية
const data = [1, 2, 3, 4, 5];
data.fill(99, 3);
console.log(data); // [1, 2, 3, 99, 99]

// إعادة تعيين كل الدرجات إلى صفر
const scores = [85, 92, 78, 100, 65];
scores.fill(0);
console.log(scores); // [0, 0, 0, 0, 0]

// إنشاء صف شبكة مملوء بقيم افتراضية
const row = new Array(8).fill('.');
console.log(row); // ['.', '.', '.', '.', '.', '.', '.', '.']
تحذير: عند الملء بكائنات أو مصفوفات، fill() يستخدم نفس المرجع لكل خانة. تعديل واحدة سيعدل الكل. استخدم Array.from({ length: n }, () => ({})) بدلا من ذلك لإنشاء كائنات مستقلة.

مثال: فخ مرجع الكائن في fill()

// خاطئ: كل الخانات تشترك في نفس مرجع الكائن
const grid = new Array(3).fill([]);
grid[0].push('X');
console.log(grid); // [['X'], ['X'], ['X']] -- الكل تغير!

// صحيح: كل خانة تحصل على مصفوفتها الخاصة
const correctGrid = Array.from({ length: 3 }, () => []);
correctGrid[0].push('X');
console.log(correctGrid); // [['X'], [], []] -- فقط الأولى تغيرت

copyWithin() -- نسخ العناصر داخل المصفوفة

تابع copyWithin() ينسخ سلسلة من العناصر داخل المصفوفة إلى موقع آخر، متجاوزا القيم الموجودة. صيغته هي array.copyWithin(target, start, end). طول المصفوفة لا يتغير. هذا تابع مُعدِّل أقل استخداما لكنه قوي لسيناريوهات محددة مثل التعامل مع المخازن المؤقتة.

مثال: استخدام copyWithin()

// نسخ العناصر من الفهرس 3 إلى الموقع 0
const arr = [1, 2, 3, 4, 5];
arr.copyWithin(0, 3);
console.log(arr); // [4, 5, 3, 4, 5]
// العناصر في الفهرس 3 و 4 (وهي 4 و 5) نُسخت إلى الفهرس 0 و 1

// نسخ العناصر من الفهرس 1 إلى الفهرس 3 (عنصران: من 1 إلى قبل 3)
const data = ['a', 'b', 'c', 'd', 'e'];
data.copyWithin(3, 1, 3);
console.log(data); // ['a', 'b', 'c', 'b', 'c']

// استخدام فهارس سلبية
const nums = [1, 2, 3, 4, 5];
nums.copyWithin(-2, 0, 2); // نسخ أول عنصرين إلى آخر موقعين
console.log(nums); // [1, 2, 3, 1, 2]

// عملي: إزاحة العناصر يسارا لـ "حذف" عنصر
const list = [10, 20, 30, 40, 50];
list.copyWithin(1, 2); // نسخ من الفهرس 2 فصاعدا إلى الفهرس 1
list.length = list.length - 1; // إزالة العنصر الأخير المكرر
console.log(list); // [10, 30, 40, 50]

concat() -- دمج المصفوفات (غير مُعدِّل)

تابع concat() يدمج مصفوفتين أو أكثر في مصفوفة جديدة دون تغيير المصفوفات الموجودة. يُرجع مصفوفة جديدة تحتوي على عناصر من المصفوفة الأصلية تليها عناصر من كل وسيط. إذا كان الوسيط مصفوفة، تُضاف عناصره بشكل فردي (تسطيح مستوى واحد). إذا لم يكن مصفوفة، يُضاف كما هو.

مثال: استخدام concat()

const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const arr3 = [7, 8, 9];

// دمج مصفوفتين
const combined = arr1.concat(arr2);
console.log(combined); // [1, 2, 3, 4, 5, 6]
console.log(arr1);     // [1, 2, 3] -- لم تتغير
console.log(arr2);     // [4, 5, 6] -- لم تتغير

// دمج مصفوفات متعددة
const all = arr1.concat(arr2, arr3);
console.log(all); // [1, 2, 3, 4, 5, 6, 7, 8, 9]

// الدمج مع قيم فردية
const withExtras = arr1.concat('a', 'b', arr2);
console.log(withExtras); // [1, 2, 3, 'a', 'b', 4, 5, 6]

// مستوى واحد فقط من التسطيح
const nested = [1, 2].concat([3, [4, 5]]);
console.log(nested); // [1, 2, 3, [4, 5]] -- المصفوفة الداخلية لم تُسطَّح

// concat مقابل الانتشار (كلاهما غير مُعدِّل)
const usingConcat = arr1.concat(arr2);
const usingSpread = [...arr1, ...arr2];
console.log(usingConcat); // [1, 2, 3, 4, 5, 6]
console.log(usingSpread); // [1, 2, 3, 4, 5, 6]
// كلاهما ينتج نفس النتيجة؛ الانتشار أكثر استخداما في الكود الحديث

slice() -- استخراج جزء (غير مُعدِّل)

تابع slice() يُرجع نسخة سطحية من جزء من المصفوفة كمصفوفة جديدة. يأخذ وسيطين اختياريين: فهرس البداية (شامل) وفهرس النهاية (حصري). إذا حذفت كليهما، ينسخ المصفوفة بأكملها. المصفوفة الأصلية لا تُعدَّل أبدا. لا تخلط بين slice() و splice() -- يبدوان متشابهين لكنهما يتصرفان بشكل مختلف تماما.

مثال: استخدام slice()

const animals = ['ant', 'bear', 'cat', 'dog', 'eagle', 'fox'];

// استخراج من الفهرس 2 إلى الفهرس 4 (حصري)
const subset = animals.slice(2, 4);
console.log(subset);  // ['cat', 'dog']
console.log(animals); // ['ant', 'bear', 'cat', 'dog', 'eagle', 'fox'] -- لم تتغير

// استخراج من الفهرس 3 إلى النهاية
const fromThree = animals.slice(3);
console.log(fromThree); // ['dog', 'eagle', 'fox']

// استخراج آخر عنصرين باستخدام فهرس سلبي
const lastTwo = animals.slice(-2);
console.log(lastTwo); // ['eagle', 'fox']

// استخراج من ما قبل الأخير إلى الأخير (حصري)
const secondToLast = animals.slice(-3, -1);
console.log(secondToLast); // ['dog', 'eagle']

// نسخ المصفوفة بأكملها
const fullCopy = animals.slice();
console.log(fullCopy); // ['ant', 'bear', 'cat', 'dog', 'eagle', 'fox']
console.log(fullCopy === animals); // false (مرجع مختلف)

// نمط شائع: إزالة عنصر بالفهرس بدون تعديل
const original = ['a', 'b', 'c', 'd', 'e'];
const indexToRemove = 2;
const withoutC = [...original.slice(0, indexToRemove), ...original.slice(indexToRemove + 1)];
console.log(withoutC); // ['a', 'b', 'd', 'e']
console.log(original); // ['a', 'b', 'c', 'd', 'e'] -- لم تتغير
نصيحة: slice() و splice() غالبا ما يتم الخلط بينهما. تذكر: slice غير مُعدِّل ويُرجع جزءا (مثل تقطيع شريحة من كعكة -- الكعكة لا تزال موجودة). splice مُعدِّل ويمكنه إضافة وإزالة واستبدال عناصر في المكان (مثل لصق حبل -- الحبل يتغير بشكل دائم).

flat() -- تسطيح المصفوفات المتداخلة (غير مُعدِّل)

تابع flat() ينشئ مصفوفة جديدة مع جميع عناصر المصفوفات الفرعية مُدمجة فيها بشكل متكرر، حتى العمق المحدد. العمق الافتراضي هو 1. لتسطيح مصفوفة متداخلة بعمق تماما، مرر Infinity كعمق. هذا تابع غير مُعدِّل قُدم في ES2019.

مثال: استخدام flat()

// تسطيح مستوى واحد عميقا (افتراضي)
const nested = [1, [2, 3], [4, [5, 6]]];
const flat1 = nested.flat();
console.log(flat1); // [1, 2, 3, 4, [5, 6]] -- مستوى واحد فقط سُطح

// تسطيح مستويين عميقا
const flat2 = nested.flat(2);
console.log(flat2); // [1, 2, 3, 4, 5, 6]

// تسطيح إلى أي عمق مع Infinity
const deeplyNested = [1, [2, [3, [4, [5]]]]];
const completelyFlat = deeplyNested.flat(Infinity);
console.log(completelyFlat); // [1, 2, 3, 4, 5]

// flat() أيضا يزيل الخانات الفارغة
const sparse = [1, , 3, , 5];
console.log(sparse.flat()); // [1, 3, 5]

// عملي: تسطيح نتائج مجمعة
const departmentEmployees = [
    ['Ali', 'Sara'],
    ['Omar', 'Layla', 'Khalid'],
    ['Nour']
];
const allEmployees = departmentEmployees.flat();
console.log(allEmployees);
// ['Ali', 'Sara', 'Omar', 'Layla', 'Khalid', 'Nour']

// الأصلية لم تتغير
console.log(departmentEmployees[0]); // ['Ali', 'Sara']

flatMap() -- الربط ثم التسطيح (غير مُعدِّل)

تابع flatMap() أولا يربط كل عنصر باستخدام دالة، ثم يُسطح النتيجة بمستوى واحد. إنه مكافئ لاستدعاء map() تليه flat(1)، لكنه أكثر كفاءة لأنه يؤدي كلتا العمليتين في تمريرة واحدة. هذا مفيد للغاية عندما تُرجع دالة الربط مصفوفات.

مثال: استخدام flatMap()

// تقسيم الجمل إلى كلمات فردية
const sentences = ['Hello world', 'How are you', 'JavaScript is awesome'];
const words = sentences.flatMap(sentence => sentence.split(' '));
console.log(words);
// ['Hello', 'world', 'How', 'are', 'you', 'JavaScript', 'is', 'awesome']

// قارن مع map (تنتج مصفوفات متداخلة)
const wordsNested = sentences.map(sentence => sentence.split(' '));
console.log(wordsNested);
// [['Hello', 'world'], ['How', 'are', 'you'], ['JavaScript', 'is', 'awesome']]

// مضاعفة كل عنصر
const nums = [1, 2, 3];
const duplicated = nums.flatMap(n => [n, n]);
console.log(duplicated); // [1, 1, 2, 2, 3, 3]

// التصفية والتحويل في خطوة واحدة (إرجاع مصفوفة فارغة للإزالة)
const scores = [85, 42, 91, 55, 78, 30, 95];
const passingGrades = scores.flatMap(score =>
    score >= 60 ? [`الدرجة: ${score}`] : []
);
console.log(passingGrades);
// ['الدرجة: 85', 'الدرجة: 91', 'الدرجة: 78', 'الدرجة: 95']

// توسيع العناصر مع تنويعاتها
const products = [
    { name: 'قميص', sizes: ['S', 'M', 'L'] },
    { name: 'قبعة', sizes: ['M', 'L'] }
];
const variants = products.flatMap(product =>
    product.sizes.map(size => `${product.name} - ${size}`)
);
console.log(variants);
// ['قميص - S', 'قميص - M', 'قميص - L', 'قبعة - M', 'قبعة - L']

فهم التعديل مقابل عدم التغيير في الممارسة

الآن بعد أن تعرف كلا من التوابع المُعدِّلة وغير المُعدِّلة، دعنا نوضح متى تستخدم كل نهج. الاختيار يعتمد على سياقك، وأسلوب البرمجة، ومتطلبات إطار العمل.

مثال: الأنماط المُعدِّلة مقابل غير المُعدِّلة جنبا إلى جنب

// --- إضافة العناصر ---
// مُعدِّل
const arr1 = [1, 2, 3];
arr1.push(4);                    // إضافة للنهاية
arr1.unshift(0);                 // إضافة للبداية
arr1.splice(2, 0, 1.5);         // إدراج في الفهرس 2
console.log(arr1); // [0, 1, 1.5, 2, 3, 4]

// غير مُعدِّل
const arr2 = [1, 2, 3];
const addEnd = [...arr2, 4];              // إضافة للنهاية
const addStart = [0, ...arr2];            // إضافة للبداية
const addMiddle = [...arr2.slice(0, 2), 1.5, ...arr2.slice(2)]; // إدراج في الفهرس 2

// --- إزالة العناصر ---
// مُعدِّل
const arr3 = [1, 2, 3, 4, 5];
arr3.pop();                      // إزالة من النهاية
arr3.shift();                    // إزالة من البداية
arr3.splice(1, 1);               // إزالة في الفهرس 1
console.log(arr3); // [2, 4]

// غير مُعدِّل
const arr4 = [1, 2, 3, 4, 5];
const noLast = arr4.slice(0, -1);           // إزالة من النهاية
const noFirst = arr4.slice(1);              // إزالة من البداية
const noIndex1 = [...arr4.slice(0, 1), ...arr4.slice(2)]; // إزالة في الفهرس 1

// --- استبدال العناصر ---
// مُعدِّل
const arr5 = ['a', 'b', 'c'];
arr5[1] = 'B';                   // استبدال مباشر
arr5.splice(0, 1, 'A');          // استبدال بـ splice

// غير مُعدِّل
const arr6 = ['a', 'b', 'c'];
const replaced = arr6.map((item, i) => i === 1 ? 'B' : item);
const replacedWithSlice = ['A', ...arr6.slice(1)];

// --- الترتيب ---
// مُعدِّل
const arr7 = [3, 1, 2];
arr7.sort((a, b) => a - b);

// غير مُعدِّل
const arr8 = [3, 1, 2];
const sorted = [...arr8].sort((a, b) => a - b);
// أو استخدم toSorted() في ES2023+
const sorted2 = arr8.toSorted((a, b) => a - b);
ملاحظة: ES2023 قدمت نظائر غير مُعدِّلة لعدة توابع مُعدِّلة: toSorted() بدلا من sort()، و toReversed() بدلا من reverse()، و toSpliced() بدلا من splice()، و with(index, value) بدلا من التعيين المباشر بالفهرس. هذه مدعومة في جميع المتصفحات الحديثة وهي النهج الموصى به للعمليات غير المُعدِّلة.

مثال واقعي: تطبيق قائمة المهام

دعونا نبني طبقة إدارة البيانات لتطبيق قائمة مهام، موضحين كيف يُستخدم كل تابع في سيناريو واقعي.

مثال: طبقة بيانات قائمة المهام الكاملة

// تهيئة قائمة المهام
let todos = [
    { id: 1, text: 'شراء البقالة', completed: false, priority: 'high' },
    { id: 2, text: 'تنظيف المنزل', completed: false, priority: 'medium' },
    { id: 3, text: 'قراءة كتاب', completed: true, priority: 'low' }
];

// إضافة مهمة جديدة (push)
let nextId = 4;
function addTodo(text, priority = 'medium') {
    todos.push({
        id: nextId++,
        text: text,
        completed: false,
        priority: priority
    });
}
addTodo('كتابة كود', 'high');
addTodo('المشي', 'low');
console.log(todos.length); // 5

// إضافة مهمة في الأعلى (unshift)
function addUrgentTodo(text) {
    todos.unshift({
        id: nextId++,
        text: text,
        completed: false,
        priority: 'urgent'
    });
}
addUrgentTodo('إصلاح خلل حرج');
console.log(todos[0].text); // 'إصلاح خلل حرج'

// إزالة مهمة بالمعرف (splice)
function removeTodo(id) {
    const index = todos.findIndex(todo => todo.id === id);
    if (index !== -1) {
        const removed = todos.splice(index, 1);
        console.log(`تمت الإزالة: "${removed[0].text}"`);
    }
}
removeTodo(3); // يزيل 'قراءة كتاب'

// تبديل الإكمال (تعديل مباشر)
function toggleTodo(id) {
    const todo = todos.find(t => t.id === id);
    if (todo) {
        todo.completed = !todo.completed;
    }
}
toggleTodo(1);
console.log(todos.find(t => t.id === 1).completed); // true

// ترتيب المهام: غير المكتملة أولا، ثم حسب الأولوية
const priorityOrder = { urgent: 0, high: 1, medium: 2, low: 3 };
function sortTodos() {
    todos.sort((a, b) => {
        // المهام غير المكتملة تأتي أولا
        if (a.completed !== b.completed) return a.completed ? 1 : -1;
        // ثم ترتيب حسب الأولوية
        return priorityOrder[a.priority] - priorityOrder[b.priority];
    });
}
sortTodos();
console.log(todos.map(t => `[${t.completed ? 'x' : ' '}] ${t.priority}: ${t.text}`));

// الحصول على المهام المكتملة (slice + نمط filter)
const completedTodos = todos.filter(t => t.completed);
console.log('المكتملة:', completedTodos.length);

// مسح كل المكتملة (splice بالعكس لتجنب مشاكل الفهرس)
function clearCompleted() {
    for (let i = todos.length - 1; i >= 0; i--) {
        if (todos[i].completed) {
            todos.splice(i, 1);
        }
    }
}
clearCompleted();
console.log('بعد مسح المكتملة:', todos.length);

مثال واقعي: سلة التسوق

دعونا نبني نظام سلة تسوق يوضح توابع المصفوفات في سياق التجارة الإلكترونية.

مثال: إدارة سلة التسوق

// هيكل بيانات سلة التسوق
let cart = [];

// إضافة منتج إلى السلة (push)
function addToCart(product, quantity = 1) {
    const existingIndex = cart.findIndex(item => item.productId === product.id);

    if (existingIndex !== -1) {
        // المنتج موجود بالفعل في السلة -- تحديث الكمية
        cart[existingIndex].quantity += quantity;
        console.log(`تم تحديث ${product.name}: الكمية = ${cart[existingIndex].quantity}`);
    } else {
        // منتج جديد -- إضافة إلى السلة
        cart.push({
            productId: product.id,
            name: product.name,
            price: product.price,
            quantity: quantity
        });
        console.log(`تمت إضافة ${product.name} إلى السلة`);
    }
}

// كتالوج المنتجات
const products = [
    { id: 101, name: 'حاسوب محمول', price: 999.99 },
    { id: 102, name: 'فأرة', price: 29.99 },
    { id: 103, name: 'لوحة مفاتيح', price: 79.99 },
    { id: 104, name: 'شاشة', price: 349.99 },
    { id: 105, name: 'سماعات', price: 149.99 }
];

// إضافة منتجات إلى السلة
addToCart(products[0]);       // حاسوب محمول
addToCart(products[1], 2);    // 2 فأرة
addToCart(products[2]);       // لوحة مفاتيح
addToCart(products[4]);       // سماعات
addToCart(products[1], 1);    // فأرة أخرى (تحديث الكمية إلى 3)

// إزالة منتج من السلة (splice)
function removeFromCart(productId) {
    const index = cart.findIndex(item => item.productId === productId);
    if (index !== -1) {
        const removed = cart.splice(index, 1)[0];
        console.log(`تمت إزالة ${removed.name} من السلة`);
        return removed;
    }
    return null;
}

// تحديث الكمية (تعديل مباشر مع splice احتياطي)
function updateQuantity(productId, newQuantity) {
    const index = cart.findIndex(item => item.productId === productId);
    if (index === -1) return;

    if (newQuantity <= 0) {
        // إزالة المنتج إذا كانت الكمية صفر أو سالبة
        cart.splice(index, 1);
    } else {
        cart[index].quantity = newQuantity;
    }
}

// حساب إجمالي السلة
function getCartTotal() {
    let total = 0;
    for (let i = 0; i < cart.length; i++) {
        total += cart[i].price * cart[i].quantity;
    }
    return total;
}

// ترتيب السلة حسب السعر (الأعلى أولا)
function sortCartByPrice() {
    cart.sort((a, b) => (b.price * b.quantity) - (a.price * a.quantity));
}

// الحصول على ملخص السلة
function getCartSummary() {
    sortCartByPrice();
    const items = cart.map(item =>
        `${item.name} x${item.quantity} = $${(item.price * item.quantity).toFixed(2)}`
    );
    return {
        items: items,
        itemCount: cart.reduce((sum, item) => sum + item.quantity, 0),
        total: getCartTotal()
    };
}

console.log(getCartSummary());
// { items: [...], itemCount: 5, total: 1349.94 }

// حفظ لقطة من السلة (نسخة غير مُعدِّلة مع slice)
const savedCart = cart.slice();

// تفريغ السلة
cart.length = 0;
console.log(cart.length);      // 0
console.log(savedCart.length); // 4 (اللقطة محفوظة)

// استعادة السلة من اللقطة (push مع الانتشار)
cart.push(...savedCart);
console.log(cart.length); // 4 (تمت الاستعادة)

// دمج السلال من مصادر مختلفة (concat)
const wishlistItems = [
    { productId: 104, name: 'شاشة', price: 349.99, quantity: 1 }
];
const mergedCart = cart.concat(wishlistItems);
console.log(mergedCart.length); // 5

تمرين عملي

ابن نظام إدارة مهام كامل باستخدام توابع المصفوفات. أنشئ مصفوفة من كائنات المهام، كل منها بخصائص: id، وtitle، وstatus (معلقة/قيد التنفيذ/مكتملة)، وassignee، وcreatedAt (استخدم new Date().toISOString()). نفذ العمليات التالية: (1) استخدم push() لإضافة 5 مهام. (2) استخدم unshift() لإضافة مهمة عاجلة في الأعلى. (3) استخدم pop() لإزالة المهمة الأخيرة وطباعتها. (4) استخدم shift() لإزالة المهمة الأولى وطباعتها. (5) استخدم splice() لإزالة مهمة بفهرسها، وإدراج مهمتين جديدتين في الفهرس 2، واستبدال المهمة في الفهرس 1. (6) استخدم sort() مع دالة مقارنة لترتيب المهام حسب الحالة (معلقة أولا، ثم قيد التنفيذ، ثم مكتملة). (7) أنشئ نسخة مُرتبة غير مُعدِّلة باستخدام عامل الانتشار و sort(). (8) استخدم slice() لاستخراج أول 3 مهام دون تعديل الأصل. (9) استخدم concat() لدمج مهامك مع مصفوفة مهام زميل. (10) أنشئ مصفوفة متداخلة من المهام مجمعة حسب الحالة، ثم استخدم flat() لتسطيحها. (11) استخدم flatMap() لإنشاء مصفوفة من السلاسل النصية مثل "مهمة: [العنوان] - [الحالة]" لكل المهام، مع تصفية المكتملة بإرجاع مصفوفات فارغة. (12) استخدم fill() لإعادة تعيين جميع حالات المهام إلى "معلقة". اختبر كل عملية وتحقق من النتائج في وحدة التحكم.

ES
Edrees Salih
منذ 23 ساعة

We are still cooking the magic in the way!