محدوده متغیرها و اجرای کد

Global Scope، Function Scope و Block Scope، Lexical Scope و Scope Chain، Hoisting، Temporal Dead Zone، Execution Context و Call Stack، Lexical Environment و Environment Record، مدیریت حافظه و Garbage Collection.

محدوده (Scope) چیست؟

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

const global = "من همه‌جا دیده می‌شوم";

function outer() {
    const outerVar = "فقط در outer";

    function inner() {
        const innerVar = "فقط در inner";
        console.log(global);   // دسترسی دارد
        console.log(outerVar); // دسترسی دارد
        console.log(innerVar); // دسترسی دارد
    }
    inner();
    // console.log(innerVar); // خطا! اینجا دیده نمی‌شود
}
outer();

انواع Scope در جاوا اسکریپت

جاوا اسکریپت مدرن سه نوع محدوده اصلی دارد:

// Global:
const a = 1;

function fn() {
    // Function Scope:
    const b = 2;

    if (true) {
        // Block Scope:
        const c = 3;
        console.log(a, b, c); // همه قابل دسترسی
    }
    // console.log(c); // خطا — c فقط در بلوک if است
}

Global Scope — محدوده سراسری

متغیرهایی که بیرون از همه توابع و بلوک‌ها تعریف می‌شوند سراسری‌اند و از هر جای کد قابل دسترسی‌اند:

const appName = "LearningCity"; // سراسری

function showApp() {
    console.log(appName); // قابل دسترسی
}

showApp(); // LearningCity

// در مرورگر، متغیرهای سراسری به window می‌چسبند:
// var سراسری → ویژگی window می‌شود
var oldStyle = "من در window هستم";
console.log(window.oldStyle); // "من در window هستم"

// let و const سراسری → سراسری‌اند ولی در window نیستند
let modern = "من در window نیستم";
console.log(window.modern); // undefined
هرچه متغیر سراسری کمتر، کد سالم‌تر! متغیرهای سراسری ممکن است ناخواسته توسط کتابخانه‌ها یا بخش‌های دیگر کد تغییر کنند و ردیابی باگ را سخت می‌کنند.

Function Scope — محدوده تابع

هر تابع محدوده خصوصی خودش را می‌سازد. متغیرهای تعریف‌شده در تابع (حتی با var) بیرون از آن نامرئی‌اند:

function calculate() {
    var result = 42;        // فقط داخل تابع
    let local = "محلی";      // فقط داخل تابع
    const PI = 3.14;        // فقط داخل تابع

    console.log(result, local, PI); // همه قابل دسترسی
}

calculate();
// console.log(result); // ReferenceError — بیرون نامرئی!

هر فراخوانی تابع، محیط جدیدی می‌سازد — متغیرهای دو فراخوانی مستقل‌اند:

let counter = 0; // سراسری — خطر تداخل!

function increment() {
    let local = 0;  // هر فراخوانی از نو شروع می‌شود
    local++;
    counter++;
    console.log("local:", local, "counter:", counter);
}

increment(); // local: 1 counter: 1
increment(); // local: 1 counter: 2 — local تازه شد!

Block Scope — محدوده بلوک

از ES6 به بعد، let و const محدوده را به سطح بلوک می‌آورند — هر جفت آکولاد { }:

// let و const بلوکی‌اند:
{
    let blockLet = "من بلوکی هستم";
    const blockConst = "من هم بلوکی‌ام";
}
// console.log(blockLet);   // خطا!
// console.log(blockConst); // خطا!

// var بلوک را نمی‌شناسد (فقط تابع را!):
{
    var functionScoped = "من از بلوک فرار می‌کنم";
}
console.log(functionScoped); // قابل دسترسی!
قانون طلایی: همیشه از const استفاده کنید؛ اگر نیاز به تغییر دارید let؛ و var را به‌کلی فراموش کنید — مگر کدهای قدیمی.

var در برابر let — تفاوت عملی

// مشکل کلاسیک var در حلقه:
for (var i = 0; i < 3; i++) {
    setTimeout(function () {
        console.log("var:", i); // 3 3 3 (!)
    }, 100);
}

// let مشکل را حل می‌کند:
for (let j = 0; j < 3; j++) {
    setTimeout(function () {
        console.log("let:", j); // 0 1 2
    }, 100);
}

چرا؟ چون var فقط یک متغیر برای کل حلقه می‌سازد ولی let در هر تکرار کپی جدیدی با مقدار آن تکرار ایجاد می‌کند.

Lexical Scope — محدوده واژگانی

محدوده در جاوا اسکریپت بر اساس جای نوشتن کد تعیین می‌شود نه جای فراخوانی — این یعنی Lexical (واژگانی/متنی) Scope:

const name = "سراسری";

function outer() {
    const name = "بیرونی";

    function inner() {
        // name اینجا از نزدیک‌ترین محیط واژگانی می‌آید:
        console.log(name); // "بیرونی" — جای تعریف مهم است!
    }

    inner();
}

outer(); // "بیرونی"

// حتی اگر inner جای دیگری فراخوانی شود:
function callElsewhere(fn) {
    const name = "جای دیگر";
    fn(); // "بیرونی" — نه "جای دیگر"!
}
callElsewhere(outer); // inner هنوز "بیرونی" را می‌بیند

Scope Chain — زنجیره محدوده

وقتی متغیری پیدا نمی‌شود، موتور جاوا اسکریپت به سمت بیرون جست‌وجو می‌کند — از محلی به سراسری. این مسیر Scope Chain نام دارد:

const level1 = "سراسری";

function first() {
    const level2 = "اول";

    function second() {
        const level3 = "دوم";

        function third() {
            console.log(level3); // نزدیک‌ترین — خودش
            console.log(level2); // یک سطح بیرون‌تر
            console.log(level1); // دو سطح بیرون‌تر
            // console.log(level0); // خطا! هرچه بالا رفت، پایین‌تر نیست
        }
        third();
    }
    second();
}
first();
زنجیره محدوده یک‌طرفه است: کد داخلی به بیرونی دسترسی دارد، اما بیرونی هرگز به متغیرهای داخلی‌تر نمی‌رسد.

Shadowing — سایه‌اندازی متغیر

وقتی متغیر داخلی هم‌نام متغیر بیرونی تعریف شود، بیرونی را می‌پوشاند:

let value = "اصلی";

function shadow() {
    let value = "سایه"; // متغیر جدید — بیرونی را می‌پوشاند
    console.log(value); // "سایه"
}

shadow();
console.log(value); // "اصلی" — دست‌نخورده!

Hoisting — بالاکشیدن

موتور جاوا اسکریپت قبل از اجرای کد، اعلان‌ها را پردازش می‌کند. به این رفتار Hoisting می‌گویند — گویی اعلان‌ها به بالای محدوده «کشیده می‌شوند». اما رفتار var و let/const و function متفاوت است:

// ۱) Function Declaration — کامل Hoist می‌شود:
hi(); // کار می‌کند!
function hi() { console.log("سلام"); }

// ۲) var — اعلان بالا می‌رود ولی مقدار نه (undefined):
console.log(a); // undefined (نه خطا!)
var a = 5;

// ۳) let/const — بالا می‌روند ولی TDZ دارند:
// console.log(b); // ReferenceError!
let b = 10;

Temporal Dead Zone (TDZ)

فاصله بین شروع محدوده تا خط تعریف متغیر let/const، «منطقه مرگ موقت» است — دسترسی در این فاصله خطا می‌دهد:

{
    // ⛔ TDZ شروع شد (age در این ناحیه «مرده» است)
    // console.log(age); // ReferenceError: Cannot access before init
    let age = 25; // ✅ TDZ تمام شد
    console.log(age); // 25 — امن
}
TDZ یک ویژگی مثبت است! به‌جای undefined مبهمِ var، فوراً خطای واضح می‌گیرید و باگ‌ها زودتر پیدا می‌شوند.

Execution Context — زمینه اجرا

موتور جاوا اسکریپت کد را در زمینه‌های اجرا (Execution Context) اجرا می‌کند. هر زمینه دو فاز دارد:

  1. فاز ایجاد (Creation): متغیرها و توابع ثبت می‌شوند، Scope Chain و this تعیین می‌شود
  2. فاز اجرا (Execution): کد خط‌به‌خط اجرا می‌شود
// زمینه سراسری (Global EC) اول ساخته می‌شود:
// سپس هر فراخوانی تابع، Function EC جدید می‌سازد:

function first() {
    console.log("اول");
    second(); // زمینه جدید برای second
}

function second() {
    console.log("دوم");
}

first();
// ترتیب ساخت زمینه‌ها: Global → first → second

Call Stack — پشته فراخوانی

پشته‌ای که زمینه‌های اجرا را مدیریت می‌کند — LIFO (آخرین وارد، اولین خارج):

function a() { b(); }
function b() { c(); }
function c() { console.log("عمق ۳"); }

a();
// Call Stack در اوج:
// [ Global ] → [ a ] → [ b ] → [ c ]
// بعد از اجرای c: c خارج، سپس b، سپس a...

فراخوانی بی‌پایان، پشته را پر می‌کند:

function recurse() {
    recurse(); // خودش را صدا می‌زند
}
recurse();
// RangeError: Maximum call stack size exceeded

Lexical Environment و Environment Record

هر زمینه اجرا یک Lexical Environment دارد — ساختاری که متغیرهای محیط را نگه می‌دارد:

// مفهومی — چگونگی نگهداری متغیرها:
// Global Environment:
//   record: { appName: "LC", ... }
//   outer: null
//
// first() Environment:
//   record: { level2: "اول" }
//   outer: → Global
//
// second() Environment:
//   record: { level3: "دوم" }
//   outer: → first

// جست‌وجوی level1 در second:
// second → نیست → first → نیست → Global → پیدا شد!

مدیریت حافظه — Memory Management

جاوا اسکریپت حافظه را خودکار مدیریت می‌کند:

  1. تخصیص (Allocation): هنگام ایجاد متغیرها و اشیا
  2. استفاده: خواندن و نوشتن
  3. آزادسازی (Release): وقتی چیزی دیگر لازم نیست
// تخصیص خودکار:
let user = { name: "علی" }; // حافظه برای شیء

// مقدارهای اولیه در Stack (سریع و کوچک):
let n = 42;
let s = "متن";

// اشیا در Heap (بزرگ و پویا):
let arr = new Array(1000);

// آزادسازی وقتی مرجع از بین برود:
user = null; // شیء قبلی دیگر در دسترس نیست → GC حذفش می‌کند

Garbage Collection — جمع‌آوری زباله

موتور (مثل V8) به‌صورت خودکار حافظه اشیای غیرقابل دسترس را آزاد می‌کند. الگوریتم اصلی Mark-and-Sweep است:

  1. Mark: از ریشه‌ها (متغیرهای سراسری، پشته فراخوانی) شروع و همه قابل‌دسترسی‌ها علامت‌گذاری می‌شوند
  2. Sweep: اشیای بدون علامت (غیرقابل دسترس) حذف می‌شوند
// شیء با یک مرجع:
let obj = { data: "مهم" };

// مرجع را از بین ببرید:
obj = null;

// حالا شیء «غیرقابل دسترس» است:
// دور بعدی GC حافظه‌اش را آزاد می‌کند ✅

// نشتی حافظه با مرجع فراموش‌شده:
const cache = [];
function process(item) {
    cache.push(item); // برای همیشه نگه می‌دارد!
    // اگر cache پاک نشود → Memory Leak
}

Memory Leak — نشتی حافظه

سه منبع رایج نشتی:

// ۱) متغیرهای سراسری ناخواسته:
function leaky() {
    leaked = "بدون let/var → سراسری!"; // ooops
}

// ۲) تایمرهای فراموش‌شده:
const timer = setInterval(() => {...}, 1000);
// اگر clearInterval(timer) نشود → برای همیشه می‌ماند

// ۳) شنونده‌های رویداد بدون حذف:
element.addEventListener("click", handler);
// اگر element حذف شود ولی listener نه → نشتی

// مرجع حذف‌شده ولی نگه‌داشته‌شده:
const detached = null;
let ref = { node: documentElement }; // اگر node حذف شود...

// راه‌حل: مرجع را پاک کنید وقتی DOM حذف می‌شود:
function cleanup() {
    ref = null; // GC می‌تواند node را جمع کند
}

// ۴) بستارهای بزرگ ناخواسته:
function heavyClosure() {
    const bigData = new Array(1000000);
    return () => bigData.length; // bigData زنده می‌ماند!
}

<h2>WeakMap و WeakSet — مراجع ضعیف</h2>
<p>برای داده‌های جانبی که نباید GC را مسدود کنند:</p>
<pre><code>const metadata = new WeakMap();

let user = { name: "علی" };
metadata.set(user, { lastLogin: "امروز" });

console.log(metadata.get(user).lastLogin); // "امروز"

user = null; // مرجع اصلی رفت
// ورودی WeakMap هم به‌مرور توسط GC پاک می‌شود — بدون نشتی!

بهترین شیوه‌ها

جمع‌بندی

در این درس عمیق‌ترین مفاهیم زیربنایی جاوا اسکریپت را آموختید: سه نوع Scope (Global، Function و Block)، زنجیره محدوده و جست‌وجوی متغیر، Lexical Scope بر اساس جای تعریف، Hoisting و TDZ، زمینه اجرا و پشته فراخوانی، و در نهایت مدیریت حافظه و Garbage Collection با الگوریتم Mark-and-Sweep. این مفاهیم پایه درک Closure، ماژول‌ها و بهینه‌سازی کد در مباحث بعدی‌اند.

این مبحث بخشی از دوره آموزش کامل جاوا اسکریپت است.

ابزارهای مرتبط