كيفية إضافة عنصر إلى بداية مصفوفة في JavaScript: تحليل تحت الغطاء

مدرب بايثون أونلاين للمبتدئين

تعلم Python بسهولة دون إرهاق نظري. حل مهام عملية مع التحقق التلقائي واكتب الكود مباشرة في المتصفح.

ابدأ الدورة

مقدمة: لماذا جمع هذا السؤال آلاف المشاهدات على Stack Overflow

\n\n

كل من كتب بلغة JavaScript ولو مرة واحدة، واجه المصفوفات. إنها في كل مكان: قوائم المنتجات، قوائم المهام، سجل إجراءات المستخدم. وعاجلاً أم آجلاً يطرح السؤال: «كيفية إضافة عنصر إلى بداية مصفوفة في JavaScript؟» على Stack Overflow، حصد هذا السؤال مئات الآلاف من المشاهدات، والعشرات من الإجابات. لماذا؟ لأنه وراء البساطة الظاهرية تكمن طبقة كاملة من المعرفة: من الأداء إلى البنية الداخلية للمحرك.

\n\n

في هذه المقالة، لن نعرض فقط unshift(). سننظر «تحت الغطاء»: كيف يعالج محرك V8 الإدراج في البداية، ولماذا قد يكون unshift أبطأ من push، وما هي البدائل المتاحة لسيناريوهات مختلفة. هيا بنا!

\n\n

الطريقة الأساسية: دالة unshift()

\n\n

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

\n\n
let fruits = ['banana', 'orange'];\nlet newLength = fruits.unshift('apple');\nconsole.log(fruits); // ['apple', 'banana', 'orange']\nconsole.log(newLength); // 3
\n\n

يمكن إضافة عدة عناصر دفعة واحدة:

\n\n
let numbers = [3, 4, 5];\nnumbers.unshift(1, 2);\nconsole.log(numbers); // [1, 2, 3, 4, 5]
\n\n

يمكننا التوقف هنا. لكن دعنا نفهم ما يحدث داخلياً عندما نستدعي unshift().

\n\n

كيف يعمل unshift() تحت الغطاء: نظرة على V8

\n\n

المصفوفة في JavaScript ليست مجرد كتلة ذاكرة متصلة كما في لغة C. في المحركات الحديثة (V8 في Chrome/Node.js، SpiderMonkey في Firefox)، يمكن تنفيذ المصفوفات بطريقتين:

\n\n
    \n
  • العناصر السريعة (المعبأة) — مخزن مؤقت متصل، على غرار نمط C. مثالية للقراءة والكتابة حسب الفهرس.
  • \n
  • عناصر القاموس (المتناثرة) — جدول تجزئة. تُستخدم عندما تصبح المصفوفة «مثقوبة» (فجوات في الفهارس) أو كبيرة جداً.
  • \n
\n\n

عند استدعاء unshift() على مصفوفة سريعة، يقوم المحرك بما يلي:

\n\n
    \n
  1. تخصيص ذاكرة جديدة (أو إزاحة الذاكرة الحالية) بعدد العناصر المضافة.
  2. \n
  3. إزاحة جميع العناصر الحالية إلى اليمين بمقدار n موضعاً.
  4. \n
  5. كتابة العناصر الجديدة في الخلايا الفارغة في البداية.
  6. \n
\n\n

لماذا unshift أبطأ من push؟

\n\n

الدالة push() تضيف عنصراً في النهاية — لا تحتاج إلى إزاحة أي شيء. أما unshift() فتعيد كتابة الفهارس لكل عنصر موجود. إذا كانت المصفوفة تحتوي على 1000 عنصر، فإن unshift يضطر لإزاحة جميع العناصر الـ 1000. بينما push يضيف ببساطة في النهاية.

\n\n

يوجد تحسين في V8: إذا كانت المصفوفة فارغة أو شبه فارغة، تكون الإزاحة مجانية تقريباً. لكن بالنسبة للمصفوفات الكبيرة، يمكن أن يكون الفرق في الأداء عشرات المرات. خاصة إذا تم استخدام unshift في حلقة.

\n\n
// سيء: O(n²) بسبب الإزاحة المستمرة\nlet arr = [];\nfor (let i = 0; i < 10000; i++) {\n  arr.unshift(i);\n}\n\n// جيد: O(n) — نضيف في النهاية، ثم نعكس\nlet arr2 = [];\nfor (let i = 0; i < 10000; i++) {\n  arr2.push(i);\n}\narr2.reverse();
\n\n

البديل: عامل الانتشار (ES6+)

\n\n

طريقة حديثة وأنيقة — استخدام عامل الانتشار ... لإنشاء مصفوفة جديدة:

\n\n
let oldArray = [2, 3, 4];\nlet newArray = [1, ...oldArray];\nconsole.log(newArray); // [1, 2, 3, 4]
\n\n

هذا الأسلوب لا يغير المصفوفة الأصلية، وهو مناسب غالباً في البرمجة الوظيفية (React, Redux). لكن تذكر: يتم إنشاء مصفوفة جديدة، ويتم نسخ العناصر القديمة. بالنسبة للمصفوفات الضخمة، قد يكون هذا مكلفاً من حيث الذاكرة.

\n\n

إضافة عدة عناصر في البداية

\n\n
let base =                

المدونات

توصيات الكتب