المستوى: مبتدئ. الكلام ده موجّه للي لسه بادئ في JavaScript أو بيتعلّم برمجة من شهور قليلة. مش محتاج تكون عارف حاجة عن الـ memory أو الـ compiler عشان تفهمه.
Hoisting في JavaScript: ليه المتغير بيطلّع undefined قبل سطر تعريفه
لو طبعت متغير قبل السطر اللي عرّفته فيه ولقيت الناتج undefined بدل ما الكود يقع بخطأ، انت قابلت الـ Hoisting. المقال ده هيخليك تفهم ليه بيحصل ده بالظبط، وإزاي تكتب كودك بحيث ميفاجئكش تاني.
المشكلة باختصار
شوف الكود ده. منطقيًا المفروض يقع، لأنك بتستخدم x قبل ما تعرّفها:
console.log(x); // إيه اللي هيطبع؟
var x = 5;
console.log(x); // وهنا؟
الناتج الفعلي على Node 22 بيطلع كده:
undefined
5
السطر الأول مرماهوش خطأ، وطبع undefined. ده اللي بيلخبط أي حد بادئ. كأن JavaScript عرفت إن في متغير اسمه x قبل ما توصل لسطر تعريفه. وده بالظبط اللي بيحصل.
افهمها الأول بمثال المخزن
تخيّل عامل في مخزن. قبل ما يبدأ شغل اليوم، بيقرأ ورقة الطلبات كلها مرة واحدة، وبيكتب على رف فاضي أسماء كل الصناديق اللي هيحتاجها النهارده. الرف دلوقتي فيه أسماء بس، من غير محتوى جوه الصناديق.
بعد كده بيبدأ ينفّذ الطلبات سطر سطر. أول ما يوصل لطلب "املأ صندوق x بـ 5 قطع"، ساعتها بس الصندوق بيتملي.
فلو حد سأل العامل عن صندوق x قبل ما يوصل لسطر التعبئة، هيلاقي الاسم موجود على الرف بس الصندوق فاضي. الاسم اتحجز بدري، القيمة لسه متحطتش. الكلمة "فاضي" دي هي undefined في JavaScript.
التعريف العلمي للـ Hoisting
بالتفاصيل: قبل ما JavaScript تشغّل كودك سطر بسطر، بتعمل مرور أولي على الـ scope كله. في المرور ده، بتحجز مكان في الذاكرة لكل تعريفات var وكل function declarations، وبترفعها مفاهيميًا لأعلى الـ scope. ده اللي اسمه Hoisting (الرفع).
المهم اللي لازم يثبت في دماغك: اللي بيتـرفع هو التعريف بس، مش القيمة. يعني var x = 5 الكومبايلر بيفهمها كأنها سطرين منفصلين:
var x; // ده بيترفع لأعلى الـ scope (الاسم اتحجز)
// ... باقي الكود ...
x = 5; // ده بيفضل مكانه (القيمة بتتحط هنا فقط)
عشان كده console.log(x) قبل سطر x = 5 بيطبع undefined مش بيقع بخطأ: الاسم موجود، القيمة لسه لأ. التعريف الرسمي ده مأخوذ من توثيق Mozilla MDN عن الـ Hoisting.
الفرق المهم: var مقابل let و const
هنا اللي لازم تركّز فيه. let و const بيتـرفعوا برضه، بس بطريقة مختلفة بتحميك. جرّب الكود ده:
console.log(y); // ❌ هنا بيقع
let y = 5;
الناتج مش undefined المرة دي، ده خطأ صريح:
ReferenceError: Cannot access 'y' before initialization
الفرق إن let و const بيتحجزلهم مكان من بداية الـ scope، بس JavaScript بتمنعك تستخدمهم لحد سطر التعريف الفعلي. المنطقة دي بين بداية الـ scope وسطر التعريف اسمها Temporal Dead Zone أو منطقة الموت الزمني. أي محاولة تقرأ المتغير فيها بترمي ReferenceError.
ده مش عيب، ده ميزة. let و const بيحوّلوا غلطة صامتة (undefined بيمشي ويلخبطك بعدين) لغلطة صريحة بتوقفك في مكانها. الفرق ده موثّق في مواصفة ECMAScript الرسمية تحت مفهوم الـ TDZ.
سيناريو واقعي: ليه ده بيوجعك فعلًا
تخيّل عندك دالة بتحسب الخصم على طلب في موقع e-commerce بـ 50 ألف زيارة في اليوم:
function calcTotal(price) {
if (price > 100) {
var discount = price * 0.1;
}
return price - discount; // discount ممكن تكون undefined
}
console.log(calcTotal(50)); // النتيجة: NaN مش 50
المشكلة: var discount اتـرفعت لأول الدالة، فهي موجودة برة الـ if كمان. لما price أقل من 100، الشرط ما بيتنفذش، فـ discount بتفضل undefined. وبعدين 50 - undefined بيطلع NaN. ده bug صامت ممكن يفضل في الـ production أسابيع. لو كنت كتبتها let discount جوه الـ if، الكود كان وقع وقت التطوير وعرّفك بالغلط بدري.
trade-offs: تكسب إيه وتخسر إيه
- استخدام let و const: بتكسب أمان وأخطاء صريحة بدري. بتخسر... تقريبًا لا شيء. ده السبب إن الكود الحديث بطّل يستخدم
var. - الاعتماد على hoisting الدوال: function declarations بتترفع بالكامل (الاسم والجسم)، فتقدر تناديها قبل تعريفها. ده بيخليك ترتب الكود بحرية، بس بيخفي ترتيب الاعتماديات على القارئ الجديد.
- الافتراض هنا: الكلام ده على JavaScript حديث (ES6 وما بعده) على Node 18 أو أحدث. الكود القديم اللي مليان
varله سلوك أصعب في التتبع.
متى متشغلش بالك بالموضوع
لو بتكتب كودك كله بـ const وبتنزل لـ let بس وقت الحاجة، وبتعرّف المتغير قبل ما تستخدمه دايمًا (وده الطبيعي)، فالـ Hoisting مش هيقابلك كمشكلة أبدًا. الموضوع ده بيهمك في حالتين: لما تقرأ كود قديم فيه var، أو لما تتسأل عنه في interview. غير كده، القاعدة البسيطة بتكفيك.
الخطوة التالية
افتح أي ملف JavaScript عندك ودوّر على كلمة var. غيّرها لـ const، ولو الكود اشتكى إنه بيعيد إسناد القيمة، غيّرها لـ let. شغّل الكود تاني. لو كل حاجة اشتغلت زي ما هي، يبقى انت دلوقتي محمي من 90% من مفاجآت الـ Hoisting. والباقي بقى مجرد معرفة بتفهم بيها كود غيرك.
المصادر
- توثيق Mozilla MDN عن الـ Hoisting: developer.mozilla.org/en-US/docs/Glossary/Hoisting
- توثيق MDN عن
letوالـ Temporal Dead Zone: developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let - مواصفة ECMAScript الرسمية (قسم Declarations and the Variable Environment): tc39.es/ecma262
- توثيق Node.js عن إصدارات V8 المدعومة: nodejs.org/en/docs