يبدأ معظم الناس تعلم JavaScript من خلال بناء الأشياء. تقوم بتوصيل زر، وجلب بعض البيانات، وتراقب تغير الـ DOM. ثم تبدأ التجريدات (abstractions) في التسرب. تظهر أخطاء (bugs) لا تبدو منطقية: متغيرات توجد قبل إعلانها، ودوال تتذكر متغيرات لا ينبغي أن تصل إليها، وكلمة this تشير إلى النافذة (window)، أو زر، أو لا شيء على الإطلاق. عادة ما تكون هذه هي اللحظة التي تدرك فيها أنك بحاجة إلى النظر "تحت الغطاء" وفهم ما يفعله المحرك (engine) حقاً.

سياق التنفيذ: الإعداد المكون من مرحلتين

عندما يواجه محرك JavaScript في متصفحك أو في Node.js نصاً برمجياً، فإنه لا يقرأ الملف ببساطة من الأعلى إلى الأسفل كما يفعل الشخص الذي يمسح صفحة بعينيه. بدلاً من ذلك، يقوم ببناء "سياق تنفيذ" (execution context)، وهو حاوية تحتوي على كل ما هو مطلوب لتشغيل جزء معين من الكود. يمر كل سياق تنفيذ بمرحلتين متمايزتين.

مرحلة إنشاء الذاكرة (Memory Creation Phase). خلال هذه المسحة الأولية، يقوم المحرك بفحص النطاق (scope) بالكامل ويخصص ذاكرة لكل إعلان متغير أو دالة يجده. إذا رأى var فإنه يحجز مساحة ويخزن undefined كعنصر نائب. وإذا رأى إعلان دالة، فإنه يخزن جسم الدالة بالكامل. هذا هو السبب في أن الدالة المعلنة باستخدام كلمة function التقليدية يمكن استدعاؤها من أسطر تظهر في وقت سابق في نفس النطاق؛ فالمحرك يعرف عنها بالفعل قبل أن يبدأ في التنفيذ.

مرحلة تنفيذ الكود (Code Execution Phase). الآن يقوم المحرك بتشغيل الكود الخاص بك سطراً بسطر. هنا تحدث عمليات التعيين (assignments)، وتُقيم التعبيرات (expressions)، وتُستدعى الدوال. إذا كتبت var name = "Alice"; فسيتم أخيراً استبدال العنصر النائب من المرحلة الأولى بالسلسلة النصية. إن فهم هذا السلوك المكون من مسحتين يزيل قدراً كبيراً من الارتباك. المحرك ليس مهملًا؛ بل يتبع روتين إعداد صارم.

المتغيرات والمنطقة الميتة زمنياً (Temporal Dead Zone)

الاختيار بين var و let و const ليس مجرد تفضيل أسلوبي. فالمتغير var له نطاق دالة (function-scoped)، مما يعني أنه يتجاهل الأقواس المتعرجة تماماً. إذا أعلنت عنه داخل كتلة if فإنه يتسرب إلى الخارج. قد يكون هذا السلوك منطقياً في بدايات JavaScript، ولكن في التطبيقات الحديثة فإنه يسبب صداعاً حقيقياً في الصيانة. أما let و const فهما محددان بنطاق الكتلة (block-scoped)، حيث يحترمان الأقواس المتعرجة ويختفيان عند انتهاء الكتلة.

هناك أيضاً فرق دقيق ولكنه حاسم في كيفية تعامل الرفع (hoisting) معهما. يتم رفع إعلانات var وتهيئتها فوراً بـ undefined. أما let و const فيتم رفعهما تقنياً أيضاً؛ فالمحرك يعرف بوجودهما قبل الوصول إلى سطر الإعلان، لكنهما لا يتم تهيئتهما. يظلان في حالة من الانتظار تسمى "المنطقة الميتة زمنياً" (temporal dead zone). إذا حاولت قراءتهما قبل تنفيذ سطر الإعلان، فستحصل على خطأ مرجعي صريح ReferenceError بدلاً من الحصول على undefined بشكل خفي. هذا الانهيار مفيد في الواقع؛ فهو يمنع المنطق من الاستمرار بناءً على بيانات غير مهيأة.

النطاق المعجمي (Lexical Scope) والإغلاقات (Closures)

يجيب النطاق (scope) على سؤال بسيط: أين يمكنني الوصول إلى هذا المتغير؟ تستخدم JavaScript النطاق المعجمي (lexical scope)، مما يعني أن حقوق الوصول الخاصة بالدالة يتم تحديدها بناءً على مكان كتابتها فعلياً في الكود المصدري، وليس بناءً على المكان الذي يتم استدعاؤها فيه. إذا قمت بتعريف دالة داخل دالة أخرى، فيمكن للدالة الداخلية الوصول إلى الخارج لقراءة المتغيرات من الدالة الأب. أما الدالة الخارجية فلا يمكنها الوصول إلى الداخل. هذه العلاقة ثابتة (static). يمكنك تمرير تلك الدالة الداخلية عبر وحدات (modules) مختلفة، أو تخزينها في متغير عالمي، واستدعاؤها من ملف مختلف تماماً، ومع ذلك ستظل تتذكر المتغيرات من النطاق الذي وُلدت فيه.

هذا السلوك ينتج عنه بشكل طبيعي "الإغلاقات" (closures). يتشكل الإغلاق عندما تحتفظ دالة داخلية بمرجع لمتغير من نطاقها الخارجي. حتى بعد انتهاء الدالة الخارجية من التنفيذ وافتراض أنه كان ينبغي جمع متغيراتها المحلية بواسطة "جامع القمامة" (garbage collector)، فإن JavaScript تحافظ عليها في الذاكرة لأن الدالة الداخلية لا تزال بحاجة إليها. الدالة الداخلية تحمل بيئتها المحيطة معها.

هذه ليست مجرد تفصيلة أكاديمية. تمنحك الإغلاقات طريقة عملية لإنشاء حالة خاصة (private state) في لغة تفتقر إلى معدلات وصول (access modifiers) صريحة.

function makeCounter() {
  let count = 0;
  return function() {
    count = count + 1;
    return count;
  };
}

const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2

هنا، يكون count مخفياً. لا يمكن لأي شيء خارج الدالة المُرجعة إعادة تعيينه أو قراءته مباشرة. هذا متغير خاص تم بناؤه من ميكانيكا النطاق وحدها.

الرفع (Hoisting) في الممارسة العملية

من الشائع سماع أن JavaScript "تنقل التصريحات إلى الأعلى". هذا نموذج ذهني مفيد، لكن الكود لا يُعاد كتابته فعلياً. خلال مرحلة إنشاء الذاكرة، يقوم المحرك ببساطة بتسجيل التصريحات قبل بدء التنفيذ. عبارة مثل var x = 5; تتصرف كما لو كان التصريح والتهيئة منفصلين؛ حيث يتم معالجة التصريح var x; مبكراً ويتم تهيئته بـ undefined ، بينما تبقى عملية التعيين x = 5; في نفس المكان الذي كتبته فيه وتعمل أثناء مرحلة التنفيذ.

بسبب هذا، يمكن أن يؤدي var إلى نتائج مفاجئة. فقد يحمل متغير مستخدم بالقرب من أعلى الدالة قيمة undefined على الرغم من وجود عملية تعيين ضخمة في الأسفل. استخدام let و const يقضي على هذا الفخ تحديداً لأن "المنطقة الميتة المؤقتة" (temporal dead zone) تجبرك على إبقاء تصريحاتك فوق أماكن استخدامها.

كيف يحصل this على قيمته

إذا كان سياق التنفيذ والنطاق (scope) يحددان أين تعيش المتغيرات، فإن this يحدد الكائن المسؤول حالياً. على عكس المتغيرات المعجمية (lexical variables)، لا يتم تحديد this بناءً على مكان كتابة الدالة، بل يتم تحديده بالكامل بناءً على كيفية استدعاء الدالة.

الربط الافتراضي (Default binding) يحدث عندما تستدعي دالة عادية ومستقلة. في الوضع غير الصارم (non-strict mode)، يعود this إلى الكائن العالمي (global object). في المتصفح، يكون هذا هو window. إذا استدعيت دالة بدون أي سياق، فقد تلمس الحالة العالمية (global state) عن طريق الخطأ دون أن تدرك ذلك.

الربط الضمني (Implicit binding) يحدث عندما تستدعي دالة كطريقة (method) على كائن. إذا كتبت user.sayName()، فإن النقطة تخبر المحرك بهدوء بضبط this لتكون user طوال مدة هذا الاستدعاء. موقع الاستدعاء يهم أكثر من موقع التعريف.

الربط الصريح (Explicit binding) يتيح لك تجاوز كل شيء يدوياً. تقوم call() و apply() باستدعاء الدالة فوراً مع إجبار this على أن تكون كائناً محدداً توفره أنت. الفرق الوحيد بينهما هو كيفية تمرير الوسائط: تأخذ call قائمة مفصولة بفاصلة، بينما تأخذ apply مصفوفة. أما bind() فتعمل بشكل مختلف؛ فهي لا تستدعي الدالة على الفور، بل تعيد دالة جديدة مع تثبيت this بشكل دائم على القيمة التي زودتها بها. هذا أمر لا يقدر بثمن لدوال الاستدعاء (callbacks) التي قد تفقد سياقها عند تمريرها.

الربط باستخدام new (New binding) يأتي موضع التنفيذ عندما تستخدم الكلمة المفتاحية new أمام استدعاء الدالة. يقوم المحرك بإنشاء كائن فارغ جديد تماماً، ويضبط ارتباط النموذج الأولي (prototype linkage) الخاص به، ويوجه this داخل المنشئ (constructor) إلى تلك النسخة الجديدة.

كتابة كود يدوم

فهم الآليات هو نصف الحرفة فقط. النصف الآخر هو كتابة كود يمكن للبشر قراءته بعد ستة أشهر من الآن.

مبدأ DRY (لا تكرر نفسك) يبدو بديهياً، لكنه يُنتهك باستمرار. إذا وجدت نفسك تكتب نفس منطق التحقق أو نمط استدعاء API في ثلاثة ملفات مختلفة، فقم باستخراجه. اكتب دالة واحدة. وجود مصدر واحد للحقيقة يعني وجود مكان واحد للتحديث عند تغير المتطلبات، وهذا يوفر الوقت بطرق يصعب المبالغة في أهميتها.

مبدأ KISS (اجعل الأمر بسيطاً) هو وسيلة دفاع ضد الأنا (ego). المعاملات الشرطية المتداخلة (Nested ternaries) والإغلاقات (closures) المكتوبة في سطر واحد قد تبدو ذكية، لكنها تكلف ساعات أثناء تصحيح الأخطاء (debugging). نمط الإغلاق الذي عرضته سابقاً قوي، ومع ذلك فإن التداخل لخمسة مستويات لمجرد أنك تستطيع ذلك هو خطأ. الكود البسيط يصمد أمام تغير أعضاء الفريق، وحوادث الإنتاج، وتنبيهات منتصف الليل عندما ينكسر شيء ما ولا يتذكر أحد السبب.

العائد الحقيقي

دراسة التنفيذ