حوزه‌ی لغوی و تودرتو: پایه‌ی فهم Scope در جاوااسکریپت

مقدمه

تا اینجا، در تمام مثال‌ها به‌طور ضمنی با مفهومی سروکار داشتیم که هنوز به‌طور مستقیم نامش را نبرده بودیم: Scope. Scope تعیین می‌کند که یک متغیر در کدام بخش از کد قابل دسترسی است. درک عمیق این مفهوم، پیش‌نیاز اصلی فهم دو مبحث بسیار مهم در ادامه‌ی این دوره است: Closure و Execution Context. به همین دلیل، این فصل را با دقت و از پایه بررسی می‌کنیم.

Scope چیست؟

Scope به‌زبان ساده یعنی «قلمرو دید» یک متغیر؛ ناحیه‌ای از کد که در آن، یک متغیر خاص قابل شناسایی و استفاده است. اگر بخواهیم از یک متغیر خارج از Scope آن استفاده کنیم، جاوااسکریپت آن را نمی‌شناسد.

function greet() {
  let message = "سلام";
  console.log(message); // در دسترس است
}

greet();
console.log(message); // خطا: message تعریف نشده است

در این مثال، متغیر message فقط داخل تابع greet تعریف شده و Scope آن هم محدود به همان تابع است؛ به همین دلیل تلاش برای دسترسی به آن از بیرون تابع با خطا مواجه می‌شود.

Lexical Scope چیست؟

جاوااسکریپت از مدلی به نام Lexical Scope (حوزه‌ی لغوی) پیروی می‌کند. کلمه‌ی Lexical به این معناست که Scope هر متغیر، بر اساس محل نوشتن کد (یعنی ساختار ظاهری و ایستای برنامه) تعیین می‌شود؛ نه بر اساس اینکه تابع از کجا فراخوانی شده است. به بیان دیگر، فقط با نگاه کردن به کد و دیدن اینکه یک تابع کجا تعریف شده، می‌توان فهمید به کدام متغیرها دسترسی دارد؛ بدون نیاز به اجرای برنامه.

Scope بر اساس محل تعریف تابع تعیین می‌شود
خروجی کنسول اینجا نمایش داده می‌شود...

تحلیل رفتار جاوااسکریپت

خروجی این کد "تهران" است، نه "اصفهان"؛ با اینکه showCity از داخل run فراخوانی شده و در لحظه‌ی فراخوانی، یک متغیر city دیگر با مقدار "اصفهان" در دسترس است. دلیل این رفتار دقیقاً همان Lexical Scope است: تابع showCity در سطح بیرونی (Global) تعریف شده، پس همیشه به city ی که در همان سطح بیرونی وجود دارد دسترسی خواهد داشت؛ فارغ از اینکه از کجا فراخوانی شود. جاوااسکریپت هرگز به «مکان فراخوانی» تابع برای تعیین Scope نگاه نمی‌کند؛ فقط به «مکان تعریف» آن نگاه می‌کند.

این ویژگی نقطه‌ی مقابل مدلی به نام Dynamic Scope است که در برخی زبان‌های دیگر وجود دارد (و جاوااسکریپت از آن پیروی نمی‌کند)؛ در آن مدل، Scope بر اساس مسیر فراخوانی توابع در زمان اجرا تعیین می‌شود، نه محل نوشتن کد.

Nested Scope — حوزه‌های تودرتو

وقتی یک تابع داخل تابع دیگری تعریف شود، Scope آن تابع داخلی، Scope تابع بیرونی را در بر می‌گیرد. به این ساختار Nested Scope (حوزه‌ی تودرتو) می‌گویند:

دسترسی به متغیر بیرونی از داخل تابع تودرتو
خروجی کنسول اینجا نمایش داده می‌شود...

تحلیل کد

تابع inner با اینکه خودش outerVar را تعریف نکرده، اما چون داخل Scope تابع outer تعریف شده، به تمام متغیرهایی که در آن Scope وجود دارند دسترسی دارد. این دسترسی، یک‌طرفه است: تابع outer نمی‌تواند به متغیرهای داخل inner دسترسی داشته باشد، اما inner می‌تواند به متغیرهای outer دسترسی داشته باشد.

function outer() {
  let outerVar = "بیرونی";

  function inner() {
    let innerVar = "درونی";
    console.log(outerVar); // در دسترس است
  }

  inner();
  console.log(innerVar); // خطا: innerVar تعریف نشده است
}

چند سطح تودرتو

این تودرتو بودن می‌تواند چندین سطح داشته باشد؛ هر سطح داخلی، به تمام سطوح بیرونی خودش دسترسی دارد:

function level1() {
  let a = "سطح ۱";

  function level2() {
    let b = "سطح ۲";

    function level3() {
      let c = "سطح ۳";
      console.log(a, b, c); // هر سه در دسترس هستند
    }

    level3();
  }

  level2();
}

level1();

در این مثال، level3 به هر سه متغیر a، b و c دسترسی دارد؛ چون به‌ترتیب داخل Scope level1، level2 و خودِ level3 تعریف شده‌اند.

Global Scope

بیرونی‌ترین سطح Scope در جاوااسکریپت، Global Scope نام دارد. متغیرهایی که مستقیماً در سطح فایل و خارج از هر تابعی تعریف شوند، در Global Scope قرار می‌گیرند و از هر نقطه‌ای از کد (داخل هر تابع، در هر سطحی از تودرتو بودن) قابل دسترسی‌اند:

let appName = "برنامه‌ی من"; // Global Scope

function showAppName() {
  console.log(appName); // در دسترس است
}

showAppName();

استفاده‌ی بیش‌ازحد از متغیرهای Global در پروژه‌های بزرگ معمولاً توصیه نمی‌شود؛ چون احتمال تداخل نام متغیرها بین بخش‌های مختلف کد را افزایش می‌دهد. این نکته در بخش Best Practices فصل بررسی خواهد شد.

بلاک به‌عنوان یک Scope مستقل

همان‌طور که در فصل متغیرها دیدیم، let و const دارای Block Scope هستند؛ یعنی هر {} (چه در if، چه در حلقه، چه یک بلاک مستقل) خودش یک Scope جداگانه محسوب می‌شود:

let x = "بیرون از بلاک";

{
  let x = "داخل بلاک";
  console.log(x); // داخل بلاک
}

console.log(x); // بیرون از بلاک

این رفتار دقیقاً نمونه‌ای دیگر از Nested Scope است؛ بلاک داخلی به x بیرونی دسترسی داشت (اگر خودش x تعریف نمی‌کرد)، اما با تعریف مجدد x در Scope داخلی‌تر، آن نسخه‌ی داخلی اولویت پیدا کرد. این رفتار در بخش بعدی، تحت عنوان Scope Chain، با جزئیات دقیق‌تری بررسی می‌شود.

Best Practices

  • متغیرها را تا حد امکان در کوچک‌ترین Scope ممکن (نزدیک‌ترین بلاک یا تابعی که واقعاً به آن‌ها نیاز دارد) تعریف کنید.
  • از تعریف متغیر در Global Scope مگر در موارد ضروری خودداری کنید تا احتمال تداخل نام کاهش یابد.

ارتباط با سایر مفاهیم

Lexical Scope پایه‌ی مفهوم بسیار مهم Scope Chain است که در درس بعدی بررسی می‌کنیم؛ یعنی زنجیره‌ای که جاوااسکریپت هنگام جست‌وجوی یک متغیر، از Scope داخلی به سمت Scope های بیرونی‌تر طی می‌کند. همچنین، Lexical Scope پایه‌ی اصلی و ضروری برای فهم Closure در فصل بعدی است؛ بدون درک درست این درس، فهم عمیق Closure ممکن نخواهد بود.

جمع‌بندی

Scope قلمرو دید یک متغیر را مشخص می‌کند و در جاوااسکریپت بر اساس مدل Lexical Scope تعیین می‌شود؛ یعنی بر اساس محل تعریف کد، نه محل فراخوانی آن. وقتی توابع یا بلاک‌ها داخل یکدیگر تودرتو شوند (Nested Scope)، سطح داخلی به تمام متغیرهای سطوح بیرونی خودش دسترسی دارد، اما این دسترسی یک‌طرفه است و سطح بیرونی نمی‌تواند به متغیرهای سطح داخلی دسترسی داشته باشد.