المستوى: مبتدئ
لو كتبت console.log(x) في أول سطر، وبعدها بسطرين كتبت var x = 5، الكود مش بيطرح خطأ. بيطبع undefined. ده مش bug في المتصفح ولا في Node.js. ده سلوك اسمه Hoisting، وفهمه بيوفّر عليك ساعات من الـ debugging مش هتعرف سببها لو ميعرفتوش.
Hoisting في JavaScript: السلوك اللي بيخلّي الكود يشتغل قبل ما يتعرّف
الكود اللي بيلخبط أي مبتدئ
console.log(name); // undefined (مش error!)
var name = "أحمد";
console.log(name); // "أحمد"
أول طبعة طلعت undefined رغم إن المتغيّر name ما اتعرّفش لسه في السطر اللي قبلها. لو جرّبت نفس الكود مع let بدل var، هتلاقي JavaScript بترميلك ReferenceError. الفرق مش عشوائي، ده ناتج عن خطوة بتعملها JavaScript قبل ما تشغّل أي سطر فعليًا.
المثال البسيط: دفتر الفهرس قبل ما تكتب فيه
تخيّل إنك بتشتري دفتر جديد. قبل ما تبدأ تكتب فيه أي حاجة، الدفتر بيكون فيه أول صفحة فاضية مكتوب عليها "الفهرس". الفهرس موجود من بداية الدفتر، حتى لو لسه ما كتبتش فيه ولا اسم. لو حد سألك: "في حاجة في الفهرس؟"، هتقول: "آه، الصفحة موجودة، بس فاضية".
JavaScript بتعمل نفس الحاجة قبل ما تشغّل الكود بتاعك. بتمر على الكود مرة بسرعة، وكل ما تلاقي تعريف var x، بتسجّل في "الفهرس" بتاعها إن في متغيّر اسمه x موجود، بس بقيمة undefined. لما الكود يشتغل فعليًا ويوصل لسطر x = 5، بتكتب القيمة الحقيقية. ده اللي بيخلّي console.log(x) قبل التعريف يطبع undefined بدل ما يكسر. الصفحة موجودة، بس فاضية.
التعريف العلمي الدقيق
JavaScript engine زي V8 (الموجود في Chrome و Node.js 22) و SpiderMonkey (في Firefox) بيشغّل أي scope في مرحلتين:
- Creation Phase: الـ engine بيمر على الكود بدون ما ينفّذ أي حاجة، وبيبني Variable Environment داخل Execution Context. بيحجز مكان في الذاكرة لكل تعريف
varويديله القيمة الابتدائيةundefined. تعريفاتfunction declarationبترفع كاملة بجسمها، يعني الجسم نفسه بيبقى متاح من بداية الـ scope. - Execution Phase: الـ engine بيشغّل السطور واحد ورا التاني، وبيغيّر القيم في الذاكرة بناءً على عمليات التعيين
=.
الـ Hoisting مش حركة فعلية للكود لأعلى الملف. ده تبسيط بيتقال للمبتدئين عشان يتذكروا السلوك. الحقيقة الدقيقة إن المتغيّر "موجود" في الذاكرة قبل ما تتنفّذ أي سطر، لكن بقيمة فاضية لحد ما يوصل سطر التعيين. ده اللي ECMAScript Specification بيسميه Variable Instantiation.
الفرق بين var و let و const في الـ Hoisting
console.log(a); // undefined (var مرفوع بقيمة undefined)
console.log(b); // ReferenceError: Cannot access 'b' before initialization
console.log(c); // ReferenceError: Cannot access 'c' before initialization
var a = 1;
let b = 2;
const c = 3;
تعريفات let و const بترفع كمان، لكن في حالة اسمها Temporal Dead Zone (TDZ). لو حاولت تستخدمهم قبل سطر التعريف، JavaScript بترميلك خطأ صريح بدل ما تطبع undefined صامتة. ده تصميم مقصود اتحط في ECMAScript 2015 عشان يمنع bugs ناتجة عن الـ Hoisting الصامت بتاع var. الـ TDZ مش غلط، دي ميزة.
الفخ الكلاسيكي: function declaration ضد function expression
sayHi(); // "أهلاً" - شغّال تمام
sayBye(); // TypeError: sayBye is not a function
function sayHi() {
console.log("أهلاً");
}
var sayBye = function() {
console.log("باي");
};
function sayHi() هي function declaration، بترفع كاملة بجسمها، فا تقدر تستدعيها قبل سطر تعريفها بدون مشكلة. لكن var sayBye = function() {} هي function expression، الـ var sayBye هي اللي بترفع بس، والقيمة بتفضل undefined لحد ما الكود يوصل لسطر التعيين. لما تستدعي undefined() بتاخد TypeError لأن undefined مش قابلة للاستدعاء.
سيناريو واقعي: امتى ده بيقع في bug فعلي
فريق عندي عنده ملف JavaScript فيه 800 سطر، حصل عليه refactor كبير، وفجأة دالة بترجع undefined بدل قيمة المستخدم. السبب كان مدفون في السطر 245:
function getUser() {
console.log(currentUser); // undefined!
// ... 30 سطر منطق
var currentUser = fetchUser();
return currentUser;
}
المتغيّر currentUser اتعرّف بـ var داخل الدالة. الـ engine رفعه لأول الدالة بقيمة undefined. السطر console.log طبع undefined رغم إن fetchUser() بترجع object صحيح. الحل كان تغيير var لـ const عشان TDZ ترمي خطأ يبيّن المشكلة فورًا في السطر 245 بدل ما المبرمج يدوّر في 800 سطر. وقت الـ debugging قبل الفهم: 3 ساعات. بعد فهم الـ Hoisting: 30 ثانية.
trade-offs: ايه اللي بتكسبه وايه اللي بتخسره
بتكسب: مرونة في ترتيب التعريفات، وقدرة على استدعاء functions قبل تعريفها (مفيد في بعض patterns زي recursive helpers أو لما تحب تحط الـ main logic فوق والـ helpers تحت).
بتخسر: وضوح الكود. أي مبرمج تاني بيقرا الكود مش هيتوقّع إن متغيّر يستخدم قبل ما يتعرّف. ده بيصعّب الـ code review والـ debugging. var بالذات بتسبب bugs صامتة لأنها بترجع undefined بدل ما ترمي خطأ.
القاعدة العملية في 2026: استخدم const في كل حاجة، let لما تحتاج تغيّر القيمة، وvar ابعد عنها خالص في كود جديد. الـ Hoisting بـ var هي حاجة موروثة من JavaScript قبل 2015، وكل linter محترم (ESLint مع قاعدة no-var) بيعتبرها رائحة كود (code smell).
متى ما يهمكش الموضوع أصلاً
لو كودك كله TypeScript، أو ES2015+ مع const و let فقط، الـ Hoisting الصامت ميكسرش حاجة. الـ TDZ بترمي خطأ واضح وقت التشغيل. لو بتستخدم ESLint مع قواعد no-use-before-define و no-var، الأخطاء بتتكشف وقت الكتابة قبل ما توصل للـ runtime أصلاً. في الحالتين دول، الـ Hoisting يفضل مفهوم نظري لازم تفهمه عشان interview أو عشان تقرا كود قديم، لكن مش مصدر bugs يومية.
الخطوة التالية
افتح أي ملف JavaScript في مشروعك ودوّر على كلمة var. غيّرها لـ const أو let حسب الاستخدام (لو القيمة بتتغير استخدم let، لو لأ استخدم const). شغّل التيستات. لو حاجة كسرت، يبقى كان فيه كود معتمد على Hoisting بطريقة مش واضحة، ودلوقتي عرفت السبب وقدرت تصلّحه. ضيف قاعدة "no-var": "error" في ملف .eslintrc عشان متقعش تاني في نفس الفخ.
المصادر
- MDN Web Docs — Hoisting: developer.mozilla.org/en-US/docs/Glossary/Hoisting
- ECMAScript 2024 Language Specification — Section 9.1 Environment Records
- MDN — Temporal Dead Zone (let / const): developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/let
- V8 Blog — Understanding ECMAScript execution contexts
- ESLint Rule
no-var: eslint.org/docs/latest/rules/no-var - Kyle Simpson, You Don't Know JS Yet — Scope & Closures (2nd Edition)