النطاق المعجمي في JavaScript: ما هو Scope في جافاسكربت فعليًا؟

دقائق القراءة: 7

مقدمة لفهم النطاق في JavaScript

قد يبدو مصطلح lexical scope معقدًا عند قراءته لأول مرة، لكنه يصبح واضحًا جدًا إذا فصلنا بين جزأيه: lexical وscope. في هذا المقال سنشرح المعنى العملي لكل منهما، ثم نربطهما معًا لفهم كيفية عمل النطاق المعجمي في JavaScript بطريقة دقيقة وسهلة.

شرح مفهوم النطاق المعجمي في جافاسكربت مع أمثلة على المتغيرات والدوال المتداخلة

ما المقصود بـ Scope في JavaScript؟

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

بعبارة أبسط:

  • Global scope هو النطاق العام الذي يمكن الوصول إليه من أجزاء واسعة من البرنامج.
  • Local scope هو النطاق المحلي الذي يقتصر الوصول إليه على جزء محدد من الشيفرة، مثل داخل دالة معينة.

مثال على النطاق العام والمحلي

// Define a variable in the global scope:
const fullName = "Oluwatobi Sofela";

// Define nested functions:
function profile() {
  function sayName() {
    function writeName() {
      return fullName;
    }
    return writeName();
  }
  return sayName();
}

في المثال السابق، تم تعريف المتغير fullName داخل global scope، لذلك يمكن الوصول إليه من أي موضع مناسب داخل الشيفرة، ما دام المسار المنطقي للنطاق يسمح بذلك.

أما الدالة writeName() فقد تم تعريفها داخل الدالة sayName()، ولهذا فهي موجودة داخل نطاق محلي مرتبط بهذه الدالة. أي أن الوصول المباشر إليها يكون من داخل sayName() فقط، وليس من كل مكان في البرنامج.

من المهم أيضًا فهم أن المحرك لا يقفز مباشرة إلى النطاق العام عند محاولة الوصول إلى fullName من داخل writeName()، بل يبحث عنه تدريجيًا عبر ما يُعرف باسم scope chain.

ما هي Scope Chain؟

تشير scope chain إلى سلسلة النطاقات التي يمر بها محرك JavaScript أثناء البحث عن تعريف متغير أو دالة، بدءًا من موضع الاستدعاء الحالي وحتى الوصول إلى النطاق العام.

مثال يوضح سلسلة النطاقات

// Define a variable in the global scope:
const fullName = "Oluwatobi Sofela";

// Define nested functions:
function profile() {
  function sayName() {
    function writeName() {
      return fullName;
    }
    return writeName();
  }
  return sayName();
}

عند استدعاء المتغير fullName من داخل الدالة writeName()، تكون سلسلة النطاقات كالتالي:

  • نطاق writeName()
  • ثم نطاق sayName()
  • ثم نطاق profile()
  • ثم global scope

وهذا يعني أن المحرك يفحص هذه البيئات بالتتابع إلى أن يجد تعريف المتغير المطلوب.

كيف تعمل Scope Chain عمليًا؟

عندما تنفّذ JavaScript الشيفرة، فإنها تبحث عن أصل المتغير وفق ترتيب هرمي واضح. لنعد إلى المثال نفسه:

// Define a variable in the global scope:
const fullName = "Oluwatobi Sofela";

// Define nested functions:
function profile() {
  function sayName() {
    function writeName() {
      return fullName;
    }
    return writeName();
  }
  return sayName();
}

عند استدعاء الدالة profile() يحدث الآتي:

  1. يتم تنفيذ sayName() لأنها موجودة داخل profile().
  2. ثم يتم تنفيذ writeName() لأنها موجودة داخل sayName().
  3. داخل writeName() يطلب المحرك قيمة fullName.
  4. يبدأ البحث أولًا داخل نطاق writeName()، فلا يجد المتغير.
  5. ينتقل بعدها إلى نطاق sayName()، ولا يجده أيضًا.
  6. ثم ينتقل إلى نطاق profile()، ولا يعثر عليه هناك.
  7. أخيرًا يصل إلى global scope، حيث يجد تعريف fullName ويعيد قيمته.

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

تمرين عملي: أي متغير سيتم استخدامه؟

انظر إلى الشيفرة التالية وحاول تحديد أي نسخة من fullName سيستخدمها المحرك:

// First fullName variable defined in the global scope:
const fullName = "Oluwatobi Sofela";

// Nested functions containing two more fullName variables:
function profile() {
  const fullName = "Tobi Sho";

  function sayName() {
    const fullName = "Oluwa Sofe";

    function writeName() {
      return fullName;
    }

    return writeName();
  }

  return sayName();
}

الإجابة الصحيحة هي أن المحرك سيستخدم المتغير fullName الموجود داخل الدالة sayName().

السبب هو أن الدالة writeName() تبدأ البحث داخل نطاقها أولًا، ثم تنتقل إلى النطاق الأعلى مباشرة، وهو نطاق sayName(). وبما أنها وجدت هناك تعريفًا للمتغير fullName، فإنها تتوقف عن البحث وتعيد القيمة "Oluwa Sofe".

ملاحظات مهمة حول النطاق

  • إذا لم يجد المحرك تعريف المتغير في أي نطاق ضمن السلسلة، فستظهر رسالة مثل Uncaught ReferenceError: fullName is not defined.
  • يُعد global scope آخر محطة في أي scope chain داخل JavaScript.
  • يمكن للنطاق الداخلي الوصول إلى النطاق الخارجي، لكن العكس غير صحيح.
  • بمعنى آخر، الدالة الابنة تستطيع استخدام ما هو متاح في الدالة الأب أو في النطاق العام، بينما لا تستطيع الدالة الأب الوصول إلى متغيرات وتعريفات الدالة الابنة مباشرة.

مراجعة سريعة لمفهوم النطاق

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

ما معنى Lexical؟

كلمة lexical ترتبط بالتعريف أو الصياغة. أي شيء متعلق بإنشاء الكلمات أو العبارات أو أسماء المتغيرات يمكن وصفه بأنه lexical.

وفي السياق البرمجي، نستخدم هذا المعنى للإشارة إلى الموضع الذي تم فيه تعريف العنصر داخل الشيفرة، وليس فقط المكان الذي جرى استدعاؤه منه.

ما هو Lexical Scope في JavaScript؟

Lexical scope هو نطاق التعريف الخاص بالعنصر البرمجي. أي أنه المكان الذي تم فيه إنشاء المتغير أو الدالة داخل الشيفرة.

ويُعرف أيضًا باسم static scope.

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

مثال أول على lexical scope

// Define a variable in the global scope:
const myName = "Oluwatobi";

// Call myName variable from a function:
function getName() {
  return myName;
}

في هذا المثال، تم تعريف المتغير myName في النطاق العام، ثم جرى استدعاؤه داخل الدالة getName().

إذًا ما هو نطاقه المعجمي؟

الإجابة: النطاق المعجمي للمتغير myName هو global scope لأنه المكان الذي تم تعريفه فيه، وليس المكان الذي تم استدعاؤه منه.

مثال ثانٍ على lexical scope

function getName() {
  const myName = "Oluwatobi";
  return myName;
}

هنا تم تعريف myName داخل الدالة getName() نفسها، لذلك يصبح نطاقه المعجمي هو النطاق المحلي لتلك الدالة.

كيف يعمل Lexical Scope؟

يعتمد الوصول إلى المتغيرات والدوال في JavaScript على البيئة التي تم تعريفها فيها. أي أن الشيفرة المسموح لها بالوصول إلى عنصر معين هي الشيفرة الموجودة داخل نطاقه المعجمي أو ضمن تسلسل الوصول الصحيح من النطاقات الداخلية إلى الخارجية.

مثال على فشل الوصول بسبب اختلاف النطاق المعجمي

// Define a function:
function showLastName() {
  const lastName = "Sofela";
  return lastName;
}

// Define another function:
function displayFullName() {
  const fullName = "Oluwatobi " + lastName;
  return fullName;
}

// Invoke displayFullName():
console.log(displayFullName());

// The invocation above will return:
// Uncaught ReferenceError: lastName is not defined

ستنتج الشيفرة السابقة الخطأ Uncaught ReferenceError لأن المتغير lastName تم تعريفه داخل الدالة showLastName()، لذلك لا يمكن للدالة displayFullName() الوصول إليه مباشرة.

السبب ليس أن المتغير غير موجود، بل لأنه خارج نطاق التعريف المسموح لتلك الدالة.

مثال صحيح على استخدام دالة من نفس النطاق

function showLastName() {
  const lastName = "Sofela";
  return lastName;
}

// Define another function:
function displayFullName() {
  const fullName = "Oluwatobi " + showLastName();
  return fullName;
}

// Invoke displayFullName():
console.log(displayFullName());

// The invocation above will return:
// "Oluwatobi Sofela"

في هذا المثال نجحت الدالة displayFullName() لأنها لم تحاول الوصول مباشرة إلى المتغير lastName، بل استدعت الدالة showLastName() التي تملك هذا المتغير داخل نطاقها المحلي ثم أعادت قيمته.

بكلمات أخرى، كل من displayFullName() وshowLastName() معرفتان في global scope، لذا تستطيع إحداهما استدعاء الأخرى. لكن هذا لا يعني أن إحداهما تملك حق الوصول المباشر إلى المتغيرات المحلية داخل الأخرى.

الفرق بين النطاق المعجمي والنطاق الديناميكي

النطاق المعجمي lexical scope هو النموذج المستخدم في JavaScript، وفيه يتم تحديد إمكانية الوصول بالاعتماد على مكان التعريف داخل الشيفرة.

أما dynamic scope فيعتمد على مسار الاستدعاء وقت التنفيذ، وهو أقل استخدامًا في لغات البرمجة الحديثة، ويظهر في بعض البيئات المحدودة مثل bash.

لماذا يُعد فهم lexical scope مهمًا؟

  • يساعدك على تفسير سلوك المتغيرات داخل الدوال المتداخلة.
  • يجعلك تتجنب أخطاء شائعة مثل ReferenceError.
  • يمنحك فهمًا أعمق لآليات مثل closures وكتابة الشيفرة المنظمة.
  • يسهّل عليك قراءة المشاريع الكبيرة التي تعتمد على تعدد النطاقات.
  • يحسن قدرتك على تصحيح الأخطاء وتحليل سبب وصول دالة إلى متغير معين دون غيره.

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

النطاق في JavaScript ليس مجرد مفهوم نظري، بل هو قاعدة أساسية تحدد كيف يرى المحرك المتغيرات والدوال أثناء التنفيذ. أما lexical scope فهو يحدد هذه الرؤية بناءً على مكان تعريف العنصر داخل الشيفرة، لا على مكان استدعائه. وكلما أتقنت فهم scope chain والعلاقة بين النطاقات الداخلية والخارجية، أصبحت قراءتك للشيفرة أدق وكتابتك للبرامج أكثر احترافية واستقرارًا.

اترك تعليقاً

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