مقدمة: لماذا جمع هذا السؤال آلاف المشاهدات على Stack Overflow
\n\nكل من كتب بلغة JavaScript ولو مرة واحدة، واجه المصفوفات. إنها في كل مكان: قوائم المنتجات، قوائم المهام، سجل إجراءات المستخدم. وعاجلاً أم آجلاً يطرح السؤال: «كيفية إضافة عنصر إلى بداية مصفوفة في JavaScript؟» على Stack Overflow، حصد هذا السؤال مئات الآلاف من المشاهدات، والعشرات من الإجابات. لماذا؟ لأنه وراء البساطة الظاهرية تكمن طبقة كاملة من المعرفة: من الأداء إلى البنية الداخلية للمحرك.
\n\nفي هذه المقالة، لن نعرض فقط unshift(). سننظر «تحت الغطاء»: كيف يعالج محرك V8 الإدراج في البداية، ولماذا قد يكون unshift أبطأ من push، وما هي البدائل المتاحة لسيناريوهات مختلفة. هيا بنا!
الطريقة الأساسية: دالة unshift()
\n\nالطريقة الأكثر وضوحاً وانتشاراً هي استخدام الدالة المدمجة unshift(). تقوم هذه الدالة بإضافة عنصر واحد أو أكثر إلى بداية المصفوفة وتعيد الطول الجديد للمصفوفة.
let fruits = ['banana', 'orange'];\nlet newLength = fruits.unshift('apple');\nconsole.log(fruits); // ['apple', 'banana', 'orange']\nconsole.log(newLength); // 3\n\nيمكن إضافة عدة عناصر دفعة واحدة:
\n\nlet numbers = [3, 4, 5];\nnumbers.unshift(1, 2);\nconsole.log(numbers); // [1, 2, 3, 4, 5]\n\nيمكننا التوقف هنا. لكن دعنا نفهم ما يحدث داخلياً عندما نستدعي unshift().
كيف يعمل unshift() تحت الغطاء: نظرة على V8
\n\nالمصفوفة في JavaScript ليست مجرد كتلة ذاكرة متصلة كما في لغة C. في المحركات الحديثة (V8 في Chrome/Node.js، SpiderMonkey في Firefox)، يمكن تنفيذ المصفوفات بطريقتين:
\n\n- \n
- العناصر السريعة (المعبأة) — مخزن مؤقت متصل، على غرار نمط C. مثالية للقراءة والكتابة حسب الفهرس. \n
- عناصر القاموس (المتناثرة) — جدول تجزئة. تُستخدم عندما تصبح المصفوفة «مثقوبة» (فجوات في الفهارس) أو كبيرة جداً. \n
عند استدعاء unshift() على مصفوفة سريعة، يقوم المحرك بما يلي:
- \n
- تخصيص ذاكرة جديدة (أو إزاحة الذاكرة الحالية) بعدد العناصر المضافة. \n
- إزاحة جميع العناصر الحالية إلى اليمين بمقدار
nموضعاً. \n - كتابة العناصر الجديدة في الخلايا الفارغة في البداية. \n
لماذا unshift أبطأ من push؟
\n\nالدالة push() تضيف عنصراً في النهاية — لا تحتاج إلى إزاحة أي شيء. أما unshift() فتعيد كتابة الفهارس لكل عنصر موجود. إذا كانت المصفوفة تحتوي على 1000 عنصر، فإن unshift يضطر لإزاحة جميع العناصر الـ 1000. بينما push يضيف ببساطة في النهاية.
يوجد تحسين في V8: إذا كانت المصفوفة فارغة أو شبه فارغة، تكون الإزاحة مجانية تقريباً. لكن بالنسبة للمصفوفات الكبيرة، يمكن أن يكون الفرق في الأداء عشرات المرات. خاصة إذا تم استخدام unshift في حلقة.
// سيء: 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طريقة حديثة وأنيقة — استخدام عامل الانتشار ... لإنشاء مصفوفة جديدة:
let oldArray = [2, 3, 4];\nlet newArray = [1, ...oldArray];\nconsole.log(newArray); // [1, 2, 3, 4]\n\nهذا الأسلوب لا يغير المصفوفة الأصلية، وهو مناسب غالباً في البرمجة الوظيفية (React, Redux). لكن تذكر: يتم إنشاء مصفوفة جديدة، ويتم نسخ العناصر القديمة. بالنسبة للمصفوفات الضخمة، قد يكون هذا مكلفاً من حيث الذاكرة.
\n\nإضافة عدة عناصر في البداية
\n\nlet base =