Hoisting و Scope در جاوا اسکریپت: درک عمیق‌تر

فهرست مطالب

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

درک کامل Hoisting و Scope نه تنها به شما کمک می‌کند تا از خطاهای رایج جلوگیری کنید، بلکه امکان نوشتن کدهای تمیزتر، قابل نگهداری‌تر و کارآمدتر را فراهم می‌آورد. این دانش عمیق شما را قادر می‌سازد تا مکانیزم داخلی جاوا اسکریپت را بهتر درک کرده و از قدرت آن به نحو احسن بهره‌برداری نمایید. در این مقاله جامع، ما به تفصیل به بررسی این دو مفهوم حیاتی، ارتباط آن‌ها با یکدیگر و پیامدهای عملی آن‌ها در توسعه جاوا اسکریپت خواهیم پرداخت. هدف ما ارائه یک درک عمیق و کاربردی است که فراتر از تعاریف سطحی رفته و شما را برای مواجهه با چالش‌های پیچیده‌تر آماده سازد.

محیط اجرای جاوا اسکریپت (Execution Context): بنیان و اساس

قبل از پرداختن به Hoisting و Scope، ضروری است که با مفهوم محیط اجرا (Execution Context) در جاوا اسکریپت آشنا شویم. هر زمان که کد جاوا اسکریپت اجرا می‌شود، در یک محیط اجرا قرار می‌گیرد. این محیط را می‌توان به عنوان یک کپسول انتزاعی در نظر گرفت که محیط فعلی اجرای کد را ارزیابی و مدیریت می‌کند. محیط اجرا شامل تمام اطلاعات لازم برای اجرای کد است، از جمله متغیرها، توابع و «این» (this) که به زمینه اجرای فعلی اشاره دارد.

جاوا اسکریپت تک‌رشته‌ای (single-threaded) است، به این معنی که در هر لحظه تنها یک محیط اجرا می‌تواند فعال باشد. هنگامی که یک تابع فراخوانی می‌شود، یک محیط اجرای جدید ایجاد شده و بر روی پشته محیط اجرا (Execution Context Stack) قرار می‌گیرد. این روند به صورت LIFO (Last-In, First-Out) عمل می‌کند؛ یعنی محیط اجرای فعلی که بالای پشته قرار دارد، اجرا می‌شود و پس از اتمام، از پشته خارج شده و کنترل به محیط اجرای قبلی باز می‌گردد.

به طور کلی، سه نوع محیط اجرا وجود دارد:

  • Global Execution Context (GEC): اولین محیط اجرای ایجاد شده. این محیط شامل کدی است که خارج از هر تابع یا آبجکتی قرار دارد. پس از بارگذاری اسکریپت، Global Execution Context ایجاد می‌شود و تا زمانی که کل برنامه یا صفحه مرورگر بسته نشود، باقی می‌ماند.
  • Function Execution Context (FEC): هر زمان که یک تابع فراخوانی می‌شود، یک محیط اجرای جدید برای آن تابع ایجاد می‌شود. این محیط، متغیرها و آرگومان‌های مخصوص به آن تابع را در خود جای می‌دهد.
  • Eval Execution Context: این محیط برای کدهایی ایجاد می‌شود که درون تابع eval() اجرا می‌شوند. استفاده از eval() در کدهای مدرن جاوا اسکریپت به دلیل مسائل امنیتی و کارایی توصیه نمی‌شود.

هر محیط اجرا دارای دو فاز اصلی است:

فاز ۱: فاز ایجاد (Creation Phase)

این فاز قبل از اجرای هر خط کد اتفاق می‌افتد. در این مرحله، موتور جاوا اسکریپت کد را اسکن کرده و اطلاعات مهمی را جمع‌آوری می‌کند. در فاز ایجاد، اتفاقات زیر رخ می‌دهند:

  • ایجاد Object محیط متغیر (Variable Environment): در این مرحله، موتور جاوا اسکریپت تمام متغیرهای اعلام شده با var و همچنین Function Declarationها را شناسایی و در حافظه ذخیره می‌کند. متغیرهای var با مقدار undefined مقداردهی اولیه می‌شوند، در حالی که Function Declarationها به طور کامل در حافظه قرار می‌گیرند.
  • ایجاد Object محیط لغوی (Lexical Environment): این Object نقش بسیار مهمی در مدیریت Scope و Hoisting ایفا می‌کند. Lexical Environment نیز مانند Variable Environment، شامل نگاشتی از identifierها (نام متغیرها و توابع) به مقادیر آن‌ها است. تفاوت اصلی و حیاتی این دو در نحوه مدیریت متغیرهای let و const است. Lexical Environment همچنین دارای یک ارجاع به Lexical Environment والد خود است که زنجیره Scope را تشکیل می‌دهد.
  • تعیین مقدار this: مقدار this (کلمه کلیدی this) برای محیط اجرا مشخص می‌شود. در Global Execution Context، this به Object سراسری (window در مرورگر یا global در Node.js) اشاره می‌کند. در Function Execution Context، مقدار this به نحوه فراخوانی تابع بستگی دارد.

فاز ۲: فاز اجرا (Execution Phase)

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

درک این دو فاز برای فهم Hoisting و Scope حیاتی است، زیرا Hoisting عمدتاً نتیجه رفتار جاوا اسکریپت در فاز ایجاد است و Scope به طور مستقیم توسط ساختار Lexical Environment و زنجیره Scope آن مدیریت می‌شود.

Hoisting: بالا بردن پرده

Hoisting مفهومی در جاوا اسکریپت است که به نظر می‌رسد تعریف متغیرها و Function Declarationها در بالای Scope (محدوده) فعلی خود “بالا کشیده” یا “حرکت داده” می‌شوند، صرف نظر از اینکه در کجای کد به طور فیزیکی تعریف شده‌اند. این بدان معناست که شما می‌توانید از یک متغیر یا تابع قبل از اینکه آن را در کد خود تعریف کنید، استفاده کنید.

در واقعیت، کد جاوا اسکریپت به طور فیزیکی حرکت نمی‌کند. Hoisting نتیجه رفتار موتور جاوا اسکریپت در فاز ایجاد (Creation Phase) محیط اجرا است. در این فاز، موتور جاوا اسکریپت کد را اسکن می‌کند و تمام Declarationها (اعلامیه‌ها) را قبل از شروع اجرای کد، در حافظه ثبت می‌کند. این پیش‌پردازش به Hoisting معروف است.

Hoisting متغیرهای var

متغیرهای اعلام شده با var به ابتدای Scope خود (که می‌تواند Global Scope یا Function Scope باشد) Hoist می‌شوند. اما نکته مهم اینجاست که فقط اعلان آن‌ها Hoist می‌شود، نه مقداردهی اولیه آن‌ها. به همین دلیل، اگر یک متغیر var را قبل از مقداردهی اولیه استفاده کنید، مقدار undefined را دریافت خواهید کرد.


console.log(myVar); // undefined
var myVar = 10;
console.log(myVar); // 10

function exampleFunc() {
    console.log(funcVar); // undefined
    var funcVar = 20;
    console.log(funcVar); // 20
}
exampleFunc();

// معادل با آنچه موتور JS در فاز ایجاد می بیند:
// var myVar;
// function exampleFunc() {
//    var funcVar;
//    console.log(funcVar);
//    funcVar = 20;
//    console.log(funcVar);
// }
// console.log(myVar);
// myVar = 10;
// console.log(myVar);
// exampleFunc();

در مثال بالا، myVar و funcVar به ابتدای Scope خود Hoist می‌شوند. بنابراین، در اولین console.log(myVar)، متغیر myVar اعلام شده اما هنوز مقداردهی نشده است، در نتیجه undefined را چاپ می‌کند. پس از مقداردهی اولیه به 10، مقدار صحیح چاپ می‌شود.

Hoisting توابع (Function Hoisting)

Hoisting برای توابع پیچیده‌تر است و به دو نوع اصلی تقسیم می‌شود: Function Declarations و Function Expressions.

Function Declarations (اعلان توابع)

Function Declarations به طور کامل Hoist می‌شوند، به این معنی که هم اعلان و هم تعریف بدنه تابع به بالای Scope خود منتقل می‌شوند. بنابراین، می‌توانید یک Function Declaration را قبل از تعریف واقعی آن در کد فراخوانی کنید.


myFunction(); // "سلام از تابع من!"

function myFunction() {
    console.log("سلام از تابع من!");
}

این رفتار به این دلیل است که در فاز ایجاد، Function Declarationها به طور کامل در حافظه ذخیره می‌شوند و آماده استفاده هستند.

Function Expressions (عبارات توابع)

Function Expressions (چه نام‌دار و چه بی‌نام) مانند متغیرهای var Hoist می‌شوند. یعنی فقط متغیری که تابع را در خود نگه می‌دارد Hoist می‌شود و با undefined مقداردهی اولیه می‌شود. خود تابع Hoist نمی‌شود.


// exampleFuncExpression(); // TypeError: exampleFuncExpression is not a function
// console.log(exampleFuncExpression); // undefined

var exampleFuncExpression = function() {
    console.log("سلام از عبارت تابع!");
};

exampleFuncExpression(); // "سلام از عبارت تابع!"

در این مثال، exampleFuncExpression مانند یک متغیر var Hoist می‌شود و در ابتدا مقدار undefined دارد. تلاش برای فراخوانی undefined به عنوان یک تابع منجر به TypeError می‌شود. این رفتار تاکید می‌کند که Function Expressions باید بعد از تعریف خود فراخوانی شوند.

let و const: منطقه مرگ موقت (Temporal Dead Zone – TDZ)

با معرفی let و const در ES6 (ECMAScript 2015)، قوانین Hoisting تغییرات قابل توجهی پیدا کردند. بر خلاف var، متغیرهای let و const نیز Hoist می‌شوند، اما با یک تفاوت مهم: آن‌ها در طول فاز ایجاد مقداردهی اولیه نمی‌شوند و در یک “منطقه مرگ موقت” (Temporal Dead Zone – TDZ) قرار می‌گیرند.

TDZ یک مفهوم منطقی است که نشان می‌دهد متغیر تا زمانی که به طور فیزیکی در کد به آن برسید و مقداردهی اولیه شود، قابل دسترسی نیست. تلاش برای دسترسی به یک متغیر let یا const قبل از مقداردهی اولیه آن در کد، منجر به خطای ReferenceError می‌شود، نه undefined.


// console.log(myLetVar); // ReferenceError: Cannot access 'myLetVar' before initialization
let myLetVar = 10;
console.log(myLetVar); // 10

// console.log(myConstVar); // ReferenceError: Cannot access 'myConstVar' before initialization
const myConstVar = 20;
console.log(myConstVar); // 20

این رفتار let و const، که به عنوان Hoisting “ایمن‌تر” شناخته می‌شود، از بسیاری از خطاهای رایج مرتبط با var جلوگیری می‌کند و کد را قابل پیش‌بینی‌تر می‌سازد. TDZ از نقطه شروع Scope (معمولاً ابتدای بلوک یا تابع) تا نقطه واقعی اعلان متغیر ادامه دارد.

Hoisting کلاس‌ها (Class Hoisting)

کلاس‌ها نیز در جاوا اسکریپت Hoist می‌شوند، اما مانند let و const، آن‌ها در TDZ قرار می‌گیرند. این بدان معنی است که نمی‌توانید یک کلاس را قبل از اعلان آن استفاده کنید.


// const myInstance = new MyClass(); // ReferenceError: Cannot access 'MyClass' before initialization

class MyClass {
    constructor() {
        console.log("نمونه کلاس ایجاد شد.");
    }
}

const myInstance = new MyClass(); // OK

این رفتار منطقی است، زیرا کلاس‌ها به طور کلی از نظر مفهومی شبیه به Function Expressions هستند، جایی که تعریف کامل آن‌ها در زمان اجرا مورد نیاز است.

Scope: تعریف مرزها

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

جاوا اسکریپت به طور سنتی دو نوع Scope اصلی داشت: Global Scope و Function Scope. با ES6، نوع جدیدی به نام Block Scope معرفی شد که با let و const کار می‌کند.

Scope سراسری (Global Scope)

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


var globalVar = "من یک متغیر سراسری هستم.";

function accessGlobal() {
    console.log(globalVar); // "من یک متغیر سراسری هستم."
}

accessGlobal();
console.log(globalVar); // "من یک متغیر سراسری هستم."

استفاده بیش از حد از Global Scope می‌تواند منجر به “آلودگی Scope” شود، جایی که نام‌های متغیرها تداخل پیدا کرده و اشکال‌زدایی را دشوار می‌کنند. این یکی از دلایلی است که استفاده از var در Global Scope توصیه نمی‌شود و let و const ترجیح داده می‌شوند.

Scope تابعی (Function Scope یا Local Scope)

متغیرهای اعلام شده با var درون یک تابع، دارای Function Scope هستند. این بدان معناست که آن‌ها فقط درون آن تابع قابل دسترسی هستند و از بیرون تابع قابل دسترسی نیستند. هر تابع یک Scope جدید ایجاد می‌کند.


function myLocalScopeFunction() {
    var localVar = "من یک متغیر محلی هستم.";
    console.log(localVar); // "من یک متغیر محلی هستم."
}

myLocalScopeFunction();
// console.log(localVar); // ReferenceError: localVar is not defined (از بیرون تابع قابل دسترسی نیست)

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

Scope بلوکی (Block Scope)

با معرفی let و const در ES6، مفهوم Block Scope به جاوا اسکریپت اضافه شد. Block Scope به این معنی است که متغیرها فقط درون بلوکی که در آن تعریف شده‌اند (مثلاً درون یک جفت آکولاد {} مانند if، for، while، یا یک بلوک ساده) قابل دسترسی هستند.


if (true) {
    let blockLet = "من در بلوک هستم (let)";
    const blockConst = "من هم در بلوک هستم (const)";
    var blockVar = "من هم در بلوک هستم، ولی نه به معنای Block Scope (var)";
    console.log(blockLet);   // "من در بلوک هستم (let)"
    console.log(blockConst); // "من هم در بلوک هستم (const)"
    console.log(blockVar);   // "من هم در بلوک هستم، ولی نه به معنای Block Scope (var)"
}

// console.log(blockLet);   // ReferenceError: blockLet is not defined
// console.log(blockConst); // ReferenceError: blockConst is not defined
console.log(blockVar);   // "من هم در بلوک هستم، ولی نه به معنای Block Scope (var)" - این به دلیل Function Scope بودن var است.

for (let i = 0; i < 3; i++) {
    // i فقط در این بلوک for وجود دارد.
    console.log(i);
}
// console.log(i); // ReferenceError: i is not defined

همانطور که مشاهده می‌کنید، blockLet و blockConst پس از بلوک if قابل دسترسی نیستند، در حالی که blockVar به دلیل رفتار var (داشتن Function Scope و نه Block Scope) همچنان قابل دسترسی است. Block Scope به توسعه‌دهندگان کمک می‌کند تا متغیرها را در کوچک‌ترین Scope ممکن تعریف کنند و از تداخل‌های غیرمنتظره جلوگیری کنند، که منجر به کدی تمیزتر و ایمن‌تر می‌شود.

Scope لغوی (Lexical Scope) و زنجیره Scope (Scope Chain)

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

هر محیط اجرا یک Lexical Environment دارد که شامل تعریف متغیرها و یک ارجاع به Lexical Environment والد است. این ارجاعات به هم پیوسته، زنجیره Scope (Scope Chain) را تشکیل می‌دهند. هنگامی که موتور جاوا اسکریپت به دنبال یک متغیر است، ابتدا در Lexical Environment فعلی جستجو می‌کند. اگر متغیر پیدا نشد، به Lexical Environment والد (با استفاده از ارجاع outer) حرکت می‌کند و این جستجو را تا رسیدن به Global Lexical Environment ادامه می‌دهد. اگر متغیر تا آن زمان پیدا نشد، یک ReferenceError پرتاب می‌شود.


const globalVariable = "من یک متغیر سراسری هستم.";

function outerFunction() {
    const outerVariable = "من در Scope بیرونی هستم.";

    function innerFunction() {
        const innerVariable = "من در Scope داخلی هستم.";
        console.log(innerVariable); // قابل دسترسی
        console.log(outerVariable); // قابل دسترسی (از Scope والد)
        console.log(globalVariable); // قابل دسترسی (از Global Scope)
    }

    innerFunction();
    // console.log(innerVariable); // ReferenceError: innerVariable is not defined
}

outerFunction();

در مثال بالا:

  • innerFunction به innerVariable دسترسی دارد زیرا در Scope خودش تعریف شده است.
  • innerFunction به outerVariable دسترسی دارد زیرا outerFunction والد لغوی innerFunction است.
  • innerFunction به globalVariable دسترسی دارد زیرا Global Scope در بالاترین نقطه زنجیره Scope قرار دارد.
  • اما از بیرون outerFunction، متغیر innerVariable قابل دسترسی نیست، زیرا innerVariable فقط در Scope مربوط به innerFunction تعریف شده است.

Lexical Scope، پایه و اساس مفهوم قدرتمند Closure را تشکیل می‌دهد.

Closures: فرزندان قدرتمند Scope

Closure یکی از پیشرفته‌ترین و قدرتمندترین مفاهیم در جاوا اسکریپت است که مستقیماً از Lexical Scope نشأت می‌گیرد. یک Closure، یک تابع است که می‌تواند به متغیرهای تعریف شده در Scope بیرونی‌اش، حتی پس از اینکه تابع بیرونی اجرای خود را به پایان رسانده باشد، دسترسی داشته باشد.

به عبارت دیگر، Closure به یک تابع "به یاد آوردن" محیط لغوی (Lexical Environment) که در آن ایجاد شده است، اجازه می‌دهد. این محیط شامل تمام متغیرهایی است که در Scope والد آن تابع در زمان ایجاد تابع داخلی وجود داشته‌اند.

نحوه کار Closures

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


function createCounter() {
    let count = 0; // این متغیر در Scope تابع createCounter است

    return function() { // این تابع داخلی یک Closure است
        count++; // به count دسترسی دارد
        return count;
    };
}

const counter1 = createCounter(); // counter1 یک Closure است
console.log(counter1()); // 1
console.log(counter1()); // 2

const counter2 = createCounter(); // counter2 یک Closure جدید و مستقل است
console.log(counter2()); // 1 (شروع از 1، مستقل از counter1)
console.log(counter1()); // 3 (counter1 همچنان به کار خود ادامه می‌دهد)

در مثال createCounter:

  • تابع createCounter() یک متغیر محلی count را تعریف می‌کند.
  • این تابع سپس یک تابع بی‌نام داخلی را برمی‌گرداند. این تابع داخلی به count دسترسی دارد، زیرا در Scope لغوی createCounter تعریف شده است.
  • هنگامی که createCounter() فراخوانی می‌شود، محیط اجرای آن ایجاد شده، count مقداردهی اولیه می‌شود و سپس تابع داخلی برگردانده می‌شود. پس از آن، محیط اجرای createCounter از پشته خارج می‌شود.
  • اما! تابع داخلی که حالا در counter1 ذخیره شده است، هنوز به Scope بیرونی (که شامل count است) ارجاع دارد. این ارجاع باعث می‌شود که count در حافظه باقی بماند و هر بار که counter1() فراخوانی می‌شود، مقدار count افزایش یابد.
  • counter2 یک Closure مستقل جدید ایجاد می‌کند، با count خودش که از 0 شروع می‌شود.

این قابلیت Closure برای ایجاد توابع دارای "حالت" (stateful functions)، ایجاد توابع خصوصی (private functions) و بسیاری از الگوهای طراحی پیچیده دیگر بسیار مفید است.

کاربردهای عملی Closures

Closures در بسیاری از الگوهای رایج جاوا اسکریپت کاربرد دارند:

۱. کپسوله‌سازی و داده‌های خصوصی (Data Encapsulation / Private Variables)

Closures راهی برای شبیه‌سازی متغیرهای خصوصی در جاوا اسکریپت (که به طور ذاتی از قابلیت‌های کلاس‌ها پشتیبانی نمی‌کرد) فراهم می‌کنند. این الگو به شما اجازه می‌دهد تا متغیرهایی را تعریف کنید که از بیرون قابل دسترسی نیستند، اما توابع داخلی می‌توانند به آن‌ها دسترسی داشته باشند.


function createPerson(name) {
    let _age = 0; // متغیر خصوصی با استفاده از Closure

    return {
        getName: function() {
            return name;
        },
        getAge: function() {
            return _age;
        },
        incrementAge: function() {
            _age++;
        }
    };
}

const person = createPerson("علی");
console.log(person.getName()); // "علی"
console.log(person.getAge()); // 0
person.incrementAge();
console.log(person.getAge()); // 1
// console.log(person._age); // undefined - _age قابل دسترسی از بیرون نیست

۲. کارخانه‌های توابع (Function Factories)

Closures به شما امکان می‌دهند توابعی را ایجاد کنید که توابع جدیدی با پیکربندی‌های خاص تولید می‌کنند.


function multiplier(factor) {
    return function(number) {
        return number * factor;
    };
}

const multiplyBy2 = multiplier(2);
console.log(multiplyBy2(5)); // 10

const multiplyBy10 = multiplier(10);
console.log(multiplyBy10(5)); // 50

۳. Event Handlers در حلقه‌ها

یکی از مثال‌های کلاسیک که مشکل رایج Hoisting/Scope را حل می‌کند، استفاده از Closures در حلقه‌ها برای ثبت Event Handler است، به خصوص با var. (با let و const این مشکل به طور خودکار حل می‌شود زیرا آن‌ها دارای Block Scope هستند.)


// مثال با var (مشکل آفرین)
// for (var i = 0; i < 3; i++) {
//    setTimeout(function() {
//        console.log("مقدار i:", i); // همیشه 3 را چاپ می‌کند
//    }, 100);
// }

// حل مشکل با Closure (برای var)
for (var i = 0; i < 3; i++) {
    (function(index) { // یک IIFE (Immediately Invoked Function Expression) ایجاد یک Closure می‌کند
        setTimeout(function() {
            console.log("مقدار i (با Closure):", index);
        }, 100);
    })(i); // مقدار i فعلی را به عنوان آرگومان به تابع فوری پاس می‌دهیم
}

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

در مثال var، بدون Closure، متغیر i در Global Scope (یا Function Scope اگر درون تابعی بزرگتر باشد) Hoist می‌شود و تا زمانی که setTimeoutها فراخوانی شوند، حلقه به اتمام رسیده و i به 3 رسیده است. اما با استفاده از IIFE، یک Closure برای هر تکرار ایجاد می‌شود که مقدار i را در آن لحظه "به یاد می‌آورد". با let، به دلیل Block Scope بودن، نیازی به IIFE نیست زیرا j در هر تکرار حلقه یک Scope جدید ایجاد می‌کند.

پیامدهای عملی و بهترین رویکردها

درک عمیق Hoisting و Scope فقط یک مسئله آکادمیک نیست؛ بلکه تأثیر مستقیمی بر کیفیت کد، اشکال‌زدایی و عملکرد برنامه شما دارد. در اینجا به برخی از پیامدهای عملی و بهترین رویکردها می‌پردازیم:

اجتناب از مشکلات Hoisting

۱. استفاده از let و const به جای var

این مهم‌ترین و ساده‌ترین راه برای جلوگیری از بسیاری از مشکلات Hoisting است. let و const به دلیل داشتن Block Scope و قرار گرفتن در Temporal Dead Zone (TDZ)، رفتار قابل پیش‌بینی‌تری دارند. این امر به ویژه در حلقه‌ها و بلوک‌های شرطی که var می‌توانست منجر به نشت متغیر شود، بسیار مفید است.


// پرهیز از این مورد:
// if (true) {
//     var x = 10;
// }
// console.log(x); // 10 - نشت متغیر به بیرون بلوک

// به جای آن:
if (true) {
    let y = 20;
    const z = 30;
    console.log(y, z);
}
// console.log(y); // ReferenceError
// console.log(z); // ReferenceError

همیشه const را به عنوان پیش‌فرض در نظر بگیرید، مگر اینکه نیاز به تغییر مقدار متغیر داشته باشید، در این صورت از let استفاده کنید. استفاده از var در کدهای جدید جاوا اسکریپت قویاً توصیه نمی‌شود.

۲. تعریف متغیرها و توابع در بالای Scope خود

حتی اگر Hoisting به شما اجازه می‌دهد متغیرها و توابع را قبل از تعریفشان استفاده کنید، بهترین تمرین این است که همیشه آن‌ها را در بالای Scope مربوطه خود تعریف کنید. این کار به خوانایی کد کمک کرده و از ابهامات ناشی از Hoisting (به ویژه برای افراد ناآشنا با آن) جلوگیری می‌کند. این الگو به عنوان "Hoisting آگاهانه" یا "Declaring variables at the top of their scope" شناخته می‌شود.


// توصیه شده:
function calculateTotal(price, quantity) {
    const taxRate = 0.05; // تعریف در بالای Scope تابع
    let total = price * quantity;

    total += total * taxRate;
    return total;
}

// پرهیز از این مورد:
// function calculateTotalBad(price, quantity) {
//     let total = price * quantity;
//     total += total * taxRate; // taxRate هنوز تعریف نشده
//     const taxRate = 0.05;
//     return total;
// }

۳. استفاده از Function Expressions به جای Function Declarations در صورت نیاز به کنترل Hoisting

اگرچه Function Declarations به طور کامل Hoist می‌شوند و اغلب راحت‌تر هستند، اما در سناریوهایی که می‌خواهید کنترل دقیق‌تری بر زمان تعریف و دسترسی به توابع داشته باشید (مانند تعریف توابع خصوصی در یک ماژول)، استفاده از Function Expressions با const می‌تواند مفید باشد تا از فراخوانی زودرس جلوگیری شود و به وضوح نشان دهید که تابع باید پس از تعریف در دسترس باشد.

بهره‌برداری از Scope برای کد قوی‌تر

۱. محدود کردن Scope متغیرها (Information Hiding)

با استفاده از Block Scope و Function Scope، متغیرها را تا حد امکان در کوچک‌ترین Scope ممکن تعریف کنید. این کار به کپسوله‌سازی اطلاعات کمک می‌کند، از تداخل نام‌ها جلوگیری می‌کند و کد را ماژولارتر و قابل نگهداری‌تر می‌سازد. هرچه Scope یک متغیر کوچک‌تر باشد، کمتر احتمال دارد که به طور ناخواسته تغییر داده شود یا در بخش دیگری از کد با آن تداخل ایجاد شود.


// خوب: متغیر result فقط درون بلوک if وجود دارد
function processData(data) {
    if (data.isValid) {
        let result = data.value * 2;
        console.log(result);
    }
    // console.log(result); // ReferenceError
}

// بد: متغیر result در Scope بزرگتر قرار می گیرد و می تواند با دیگر متغیرها تداخل داشته باشد
// function processDataBad(data) {
//     let result; // تعریف در Scope بزرگتر
//     if (data.isValid) {
//         result = data.value * 2;
//     }
//     console.log(result); // می تواند undefined باشد اگر data.isValid نباشد
// }

۲. استفاده از Closures برای مدیریت State و Private Data

Closures ابزار قدرتمندی برای ایجاد الگوهای ماژولار و مدیریت State در جاوا اسکریپت هستند. آن‌ها به شما اجازه می‌دهند تا داده‌ها و توابع را کپسوله کرده و یک API عمومی را برای تعامل با آن‌ها فراهم کنید، در حالی که جزئیات داخلی را پنهان نگه می‌دارید.


// ماژول با استفاده از Closure
const counterModule = (function() {
    let privateCounter = 0; // متغیر خصوصی

    function changeBy(val) {
        privateCounter += val;
    }

    return {
        increment: function() {
            changeBy(1);
        },
        decrement: function() {
            changeBy(-1);
        },
        value: function() {
            return privateCounter;
        }
    };
})(); // IIFE برای ایجاد یک Closure فوری

console.log(counterModule.value()); // 0
counterModule.increment();
counterModule.increment();
console.log(counterModule.value()); // 2
counterModule.decrement();
console.log(counterModule.value()); // 1
// console.log(counterModule.privateCounter); // undefined - غیر قابل دسترسی

۳. درک زنجیره Scope برای بهینه‌سازی عملکرد (پیشرفته)

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

var در مقابل let در مقابل const: انتخاب قطعی

با توجه به تمام بحث‌هایی که در مورد Hoisting و Scope داشتیم، انتخاب بین var، let و const تقریباً واضح است:

  • const: این انتخاب پیش‌فرض شما برای هر متغیری باید باشد. از const برای هر مقداری استفاده کنید که قصد ندارید آن را پس از مقداردهی اولیه تغییر دهید. این کار به خوانایی کد کمک می‌کند و به دیگران (و خودتان) نشان می‌دهد که این مقدار ثابت است. (توجه: const فقط به معنای "ثابت بودن ارجاع" است، نه "ثابت بودن مقدار". برای آبجکت‌ها و آرایه‌ها، می‌توانید محتوای آن‌ها را تغییر دهید، اما نمی‌توانید ارجاع به آبجکت/آرایه جدید را اختصاص دهید.)
  • let: اگر می‌دانید که مقدار یک متغیر نیاز به تغییر در طول زمان دارد (مثلاً در یک حلقه، یک شمارنده، یا یک متغیر وضعیت)، از let استفاده کنید. let Block-Scoped است و از مشکلات Hoisting var جلوگیری می‌کند.
  • var: از var اصلاً استفاده نکنید در کدهای جدید. دلیل اصلی استفاده از var (پشتیبانی مرورگرهای قدیمی) دیگر موضوعیت ندارد. var دارای Function Scope است و نه Block Scope، که منجر به رفتارهای غیرمنتظره و خطاهای رایج می‌شود. اگر با کد قدیمی کار می‌کنید که از var استفاده می‌کند، رفتار آن را درک کنید، اما آن را در کدهای جدید خود تکرار نکنید.

با پایبندی به این رویکردهای مدرن، می‌توانید کدهای جاوا اسکریپت بنویسید که نه تنها صحیح کار می‌کنند، بلکه قابل فهم‌تر، قابل نگهداری‌تر و مقاوم‌تر در برابر خطا هستند.

نتیجه‌گیری: تسلط بر اصول جاوا اسکریپت

Hoisting و Scope دو مفهوم جدایی‌ناپذیر در جاوا اسکریپت هستند که بنیان نحوه مدیریت و دسترسی به متغیرها و توابع در طول چرخه حیات برنامه شما را تشکیل می‌دهند. Hoisting، نتیجه فاز ایجاد محیط اجرا، نحوه پردازش Declarationها توسط موتور جاوا اسکریپت را قبل از اجرای کد بیان می‌کند. در حالی که Scope (شامل Global، Function و Block Scope) و Lexical Scope، قوانین دسترسی و دیداری متغیرها را تعیین می‌کنند و پایه و اساس زنجیره Scope را بنا می‌نهند.

همپوشانی این دو مفهوم به خصوص در مورد let و const و مفهوم Temporal Dead Zone حیاتی است، که نشان‌دهنده تکامل زبان جاوا اسکریپت به سمت پیش‌بینی‌پذیری و ایمنی بیشتر است. Closures، به عنوان یک پیامد مستقیم Lexical Scope، ابزاری فوق‌العاده قدرتمند برای کپسوله‌سازی داده‌ها، ایجاد Stateful Functions و ساخت الگوهای ماژولار فراهم می‌آورند.

یک توسعه‌دهنده جاوا اسکریپت حرفه‌ای، تنها نباید بداند که Hoisting و Scope چه هستند، بلکه باید به طور عمیق درک کند که چرا آن‌ها به این شکل عمل می‌کنند، چگونه با یکدیگر تعامل دارند و چگونه می‌توان از آن‌ها برای نوشتن کدهای تمیزتر، کارآمدتر و قابل نگهداری‌تر استفاده کرد. با اتخاذ بهترین رویکردها، مانند ترجیح let و const بر var و محدود کردن Scope متغیرها تا حد امکان، می‌توانید از بسیاری از مشکلات رایج جلوگیری کرده و کدی بنویسید که هم از نظر عملکرد قوی و هم از نظر منطق شفاف باشد. تسلط بر این مفاهیم، گام بزرگی در مسیر تبدیل شدن به یک توسعه‌دهنده جاوا اسکریپت ماهر و مطمئن است.

“تسلط به برنامه‌نویسی پایتون با هوش مصنوعی: آموزش کدنویسی هوشمند با ChatGPT”

قیمت اصلی 2.290.000 ریال بود.قیمت فعلی 1.590.000 ریال است.

"تسلط به برنامه‌نویسی پایتون با هوش مصنوعی: آموزش کدنویسی هوشمند با ChatGPT"

"با شرکت در این دوره جامع و کاربردی، به راحتی مهارت‌های برنامه‌نویسی پایتون را از سطح مبتدی تا پیشرفته با کمک هوش مصنوعی ChatGPT بیاموزید. این دوره، با بیش از 6 ساعت محتوای آموزشی، شما را قادر می‌سازد تا به سرعت الگوریتم‌های پیچیده را درک کرده و اپلیکیشن‌های هوشمند ایجاد کنید. مناسب برای تمامی سطوح با زیرنویس فارسی حرفه‌ای و امکان دانلود و تماشای آنلاین."

ویژگی‌های کلیدی:

بدون نیاز به تجربه قبلی برنامه‌نویسی

زیرنویس فارسی با ترجمه حرفه‌ای

۳۰ ٪ تخفیف ویژه برای دانشجویان و دانش آموزان