شرح سياق التنفيذ والرفع في JavaScript مع أمثلة عملية

دقائق القراءة: 6
غلاف توضيحي لمقال يشرح سياق التنفيذ والرفع في جافاسكربت

تُعد JavaScript من أكثر لغات البرمجة سهولةً من حيث البدء، لكنها تحتوي على مفاهيم أساسية لا بد من فهمها بعمق إذا كنت ترغب في كتابة شيفرة أوضح، وتصحيح الأخطاء بسرعة، وبناء منطق برمجي متماسك. من أهم هذه المفاهيم: Execution Context وHoisting.

فهم هذين المفهومين لا يساعدك فقط على قراءة الشيفرة بشكل أفضل، بل يمهّد أيضاً لاستيعاب مفاهيم أخرى مثل this وscope وclosure بطريقة أسهل وأكثر دقة.

ما هو سياق التنفيذ Execution Context في JavaScript؟

عند كتابة ملف برمجي بلغة JavaScript، فإنك تنشئ متغيرات ودوال وكائنات ومصفوفات وتعليمات متنوعة. لكن هذه الشيفرة لا تُنفّذ مباشرة كما هي مكتوبة، بل تمر بعدة مراحل داخل المحرك البرمجي حتى تصبح قابلة للتنفيذ.

من المهم هنا التمييز بين مفهومين مترابطين:

  • Lexical Environment: يصف المكان البنيوي الذي كُتبت فيه العناصر داخل الشيفرة.
  • Execution Context: البيئة الفعلية التي تُنفَّذ فيها الشيفرة لحظة التشغيل.

انظر إلى المثال التالي:

function doSomething() {
  var age = 7;
  // Some more code
}

في هذا المثال، المتغير age موجود من الناحية التركيبية داخل الدالة doSomething(). هذا التمركز البنيوي هو جزء من Lexical Environment. لكن تنفيذ الشيفرة نفسها لا يحدث إلا عندما ينشأ Execution Context مناسب.

رسم متحرك يوضح الفرق بين البيئة المعجمية وسياق التنفيذ في جافاسكربت

ماذا يحدث داخل سياق التنفيذ؟

حين يبدأ المحرك بتنفيذ الشيفرة، فإنه لا يقرأها قراءة سطحية فقط، بل يمررها عبر مراحل داخلية متتابعة، منها:

  1. التجزئة أو Tokenizing: تحويل الشيفرة إلى وحدات صغيرة مفهومة تسمى Tokens.
  2. التحليل أو Parsing: تحويل هذه الوحدات إلى شجرة بنيوية تُسمى AST اختصاراً لـ Abstract Syntax Tree.
  3. توليد الشيفرة القابلة للتنفيذ أو Code Generation: تحويل البنية السابقة إلى byte-code يمكن لمحرك اللغة تشغيله، ثم تحسينه لاحقاً عبر JIT.

مثال بسيط على سطر برمجي يمر بهذه المراحل:

var age = 7;

رسم يوضح انتقال الشيفرة المصدرية إلى بايت كود قابل للتنفيذ في جافاسكربت

لهذا يمكننا القول إن Execution Context هو الإطار الذي تُدار داخله عملية التهيئة والتنفيذ الفعلي للشيفرة.

أنواع سياق التنفيذ في JavaScript

يوجد نوعان أساسيان من سياق التنفيذ:

  • Global Execution Context ويُختصر إلى GEC.
  • Function Execution Context ويُختصر إلى FEC.

وكل نوع منهما يمر بمرحلتين رئيسيتين:

  • مرحلة الإنشاء أو Creation Phase.
  • مرحلة التنفيذ أو Execution Phase.

سياق التنفيذ العام Global Execution Context

بمجرد تشغيل أي ملف JavaScript، ينشئ المحرك سياق تنفيذ عاماً. هذا السياق هو الأساس الذي تبدأ منه بقية العمليات.

مرحلة الإنشاء في السياق العام

في هذه المرحلة، تحدث عدة أمور مهمّة:

  • إنشاء كائن عام يُسمى window في بيئة المتصفح.
  • إنشاء المتغير الخاص this.
  • حجز مساحة في الذاكرة للمتغيرات المعلنة.
  • إسناد القيمة الخاصة undefined إلى متغيرات var قبل التنفيذ الفعلي.
  • تخزين تعريفات الدوال المعلنة بصيغة function declaration مباشرة في الذاكرة.

مرحلة التنفيذ في السياق العام

في هذه المرحلة يبدأ تنفيذ الأسطر الفعلية، مثل إسناد القيم إلى المتغيرات وتشغيل التعليمات. أما الدوال فلا تُنفذ إلا عند استدعائها.

مثال: تحميل ملف برمجي فارغ

إذا أنشأت ملفاً باسم index.js وتركته فارغاً، ثم ربطته بملف HTML كما يلي:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta http-equiv="X-UA-Compatible" content="IE=edge" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Document</title>
  <script src="./index.js"></script>
</head>
<body>
  I'm loading an empty script
</body>
</html>

فعند تحميل الصفحة، سيُنشأ Global Execution Context حتى لو كان الملف البرمجي فارغاً. ويمكنك من خلال أدوات المطور في المتصفح كتابة window أو this في وحدة التحكم لملاحظة أنهما يشيران إلى الكائن العام نفسه داخل هذا السياق.

نافذة وحدة التحكم تعرض كائن window في المتصفحعرض قيمة this في وحدة تحكم المتصفح داخل السياق العامتوضيح أن window يساوي this داخل السياق العام في جافاسكربت

الاستنتاج هنا بسيط: حتى الملف الفارغ يمر بمرحلة إنشاء للسياق العام، وفيها يتم تجهيز window وthis.

مثال: متغيرات ودوال داخل السياق العام

var blog = 'freeCodeCamp';

function logBlog() {
  console.log(this.blog);
}

في مرحلة الإنشاء:

  • يُنشأ window وthis.
  • تُحجز الذاكرة للمتغير blog.
  • يُسند إلى blog مبدئياً القيمة undefined.
  • يُخزَّن تعريف الدالة logBlog() في الذاكرة مباشرة.

وفي مرحلة التنفيذ:

  • تُسنَد القيمة 'freeCodeCamp' إلى المتغير blog.
  • لا تُنفَّذ الدالة logBlog() لأنها لم تُستدعَ بعد.

سياق تنفيذ الدالة Function Execution Context

عند استدعاء أي دالة، ينشأ سياق تنفيذ جديد خاص بها. هذا يعني أن كل استدعاء للدالة يملك بيئته التنفيذية المستقلة.

لنوسّع المثال السابق:

var blog = 'freeCodeCamp';

function logBlog() {
  console.log(this.blog);
}

logBlog();

بمجرد استدعاء logBlog()، يُنشأ Function Execution Context جديد. هذا السياق يمر أيضاً بمرحلتين: الإنشاء ثم التنفيذ.

ما الذي يميّز سياق تنفيذ الدالة؟

  • يوفر الكائن الخاص arguments الذي يحتوي على المعاملات المرسلة إلى الدالة.
  • يملك نطاقاً خاصاً يحدد كيفية الوصول إلى المتغيرات.
  • يمكنه الوصول إلى ما هو متاح من السياق الخارجي وفق قواعد scope.

وإذا استدعت دالة دالةً أخرى، فإن المحرك ينشئ سياق تنفيذ جديداً لكل استدعاء جديد. هذه السلسلة هي ما يفسر كثيراً من سلوك المتغيرات أثناء التشغيل.

ما هو الرفع Hoisting في JavaScript؟

مصطلح Hoisting قد يوحي خطأً بأن المحرك يرفع الشيفرة فعلياً إلى أعلى الملف. لكن ما يحدث في الواقع مختلف: المحرك يجهّز بعض التعريفات في الذاكرة خلال Creation Phase قبل بدء التنفيذ سطراً بسطر.

بمعنى آخر، الرفع ليس نقلاً فعلياً للأسطر، بل نتيجة مباشرة لطريقة إنشاء Execution Context.

رفع المتغيرات Variable Hoisting

تأمل المثال التالي:

console.log(name);
var name;

الناتج سيكون:

undefined

السبب هو أن المتغير name حُجزت له ذاكرة في مرحلة الإنشاء، وأُعطيت له القيمة الابتدائية undefined قبل الوصول إلى سطر console.log(name).

إذن، رفع المتغيرات يعني:

  • التصريح باستخدام var يُجهّز في الذاكرة مبكراً.
  • لكن الإسناد الفعلي للقيمة لا يحدث إلا في مرحلة التنفيذ.

مثلاً:

name = 'freeCodeCamp';

في هذه الحالة، تُسنَد القيمة أثناء التنفيذ، بعد أن يكون المتغير قد أُنشئ مسبقاً في الذاكرة.

رفع الدوال Function Hoisting

الأمر نفسه ينطبق على الدوال المعلنة باستخدام function declaration. انظر إلى المثال التالي:

functionA();

function functionA() {
  console.log('Function A');
  functionB();
}

function functionB() {
  console.log('Function B');
}

الناتج سيكون:

Function A
Function B

لماذا نجح هذا الاستدعاء رغم أن الدوال كُتبت بعد الاستخدام؟ لأن تعريفات الدوال الكاملة توضع في الذاكرة خلال مرحلة الإنشاء، ثم تُنفَّذ عند استدعائها.

وهنا تظهر فائدة فهم الفرق بين:

  • function declaration: يتم رفعه كاملاً.
  • function initialization أو الدوال المخزّنة داخل متغير: لا تتصرف بالطريقة نفسها.

قواعد مهمّة لفهم الرفع بشكل صحيح

1) اكتب التصريحات قبل الاستخدام كلما أمكن

رغم أن الرفع قد يسمح أحياناً باستخدام العنصر قبل تعريفه الظاهري، فإن أفضل ممارسة هي إعلان المتغيرات والدوال قبل استعمالها. هذا يجعل الشيفرة أوضح وأسهل في الصيانة.

2) الرفع لا يعني رفع التهيئة

انظر إلى المثال التالي:

logMe();

var logMe = function() {
  console.log('Logging...');
};

هذا المثال سيسبب خطأ، لأن logMe هنا متغير تم التصريح عنه باستخدام var، وليس دالة معلنة بصيغة function declaration. لذلك يُرفع المتغير فقط ويُعطى القيمة undefined، ثم يحاول المحرك استدعاء undefined كما لو كانت دالة.

خطأ ناتج عن محاولة استدعاء دالة مخزنة في متغير قبل التهيئة

3) التعامل مع let وconst مختلف

تأمل هذا المثال:

console.log(name);
let name;

هنا ستحصل على خطأ من النوع ReferenceError. والسبب أن المتغيرات المعلنة باستخدام let وconst تُرفع من حيث البنية الداخلية، لكنها لا تُهيأ بالقيمة undefined بالطريقة نفسها التي يحدث بها الأمر مع var.

خطأ ReferenceError عند الوصول إلى متغير let قبل تعريفه

لذلك فإن استخدام let وconst يساعد على تقليل الأخطاء الناتجة عن الوصول غير المقصود إلى متغيرات قبل تهيئتها.

مقارنة سريعة بين var وlet وconst في الرفع

نوع التصريح هل يُرفع؟ القيمة الابتدائية قبل التنفيذ النتيجة عند الوصول المبكر
var نعم undefined يعرض undefined
let نعم لا تُتاح مباشرة خطأ ReferenceError
const نعم لا تُتاح مباشرة خطأ ReferenceError
function declaration نعم التعريف الكامل للدالة يمكن استدعاؤها قبل موضعها النصي

أفضل الممارسات عند التعامل مع سياق التنفيذ والرفع

  • احرص على تعريف المتغيرات والدوال قبل استخدامها.
  • فضّل استخدام let وconst عند الحاجة لتقليل السلوكيات المربكة.
  • لا تعتمد على الرفع كوسيلة تنظيم، بل اعتبره سلوكاً داخلياً يجب فهمه فقط.
  • فرّق دائماً بين function declaration وfunction expression.
  • عند تصحيح الأخطاء، فكّر في مرحلتَي Creation Phase وExecution Phase بدلاً من النظر إلى الشيفرة كسطور متتابعة فقط.

لماذا يُعد فهم هذه المفاهيم مهماً للمطور؟

إذا كنت تتساءل لماذا تعمل شيفرة معيّنة رغم أن الاستدعاء سبق التعريف، أو لماذا يظهر undefined أحياناً ويظهر ReferenceError أحياناً أخرى، فإن الجواب غالباً يرتبط مباشرة بـ Execution Context وHoisting.

فهم هذين المفهومين يمنحك قدرة أفضل على:

  • تحليل سلوك المحرك أثناء التشغيل.
  • توقّع الأخطاء قبل وقوعها.
  • تحسين أسلوب كتابة الشيفرة.
  • استيعاب مفاهيم متقدمة مثل scope chain وclosure وارتباط this.

الخلاصة التقنية

Execution Context هو الإطار الذي يحدد كيف تُجهَّز الشيفرة وتُنفَّذ داخل محرك JavaScript، بينما يُعد Hoisting أثراً مباشراً لمرحلة الإنشاء داخل هذا السياق. من الناحية العملية، لا ينبغي الاعتماد على الرفع في تصميم الشيفرة، لكن فهمه ضروري لتفسير سلوك المتغيرات والدوال بدقة. كلما استوعبت هذا الأساس جيداً، أصبحت قراءتك لشيفرة JavaScript أكثر احترافية، وقلت الأخطاء المنطقية التي تظهر أثناء التطوير.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *