هویستینگ و ناحیه‌ی مرده‌ی زمانی: رفتار پنهان متغیرها پیش از اجرا

مقدمه

در فصل توابع، وقتی Function Declaration و Function Expression را مقایسه کردیم، دیدیم که یک تابع Declaration را می‌توان پیش از تعریف آن فراخوانی کرد، اما تلاش برای فراخوانی زودهنگام یک Function Expression با خطا مواجه می‌شود. در آن درس گفتیم که علت این رفتار در فصل Execution Model به‌طور کامل توضیح داده خواهد شد. اکنون وقت آن رسیده که این رفتار را از ریشه بفهمیم: مفهومی به نام Hoisting.

Hoisting چیست؟

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

Hoisting در Function Declaration

Function Declaration به‌طور کامل Hoist می‌شود؛ یعنی هم نام تابع و هم کل بدنه‌ی آن، پیش از اجرای کد در دسترس قرار می‌گیرد:

sayHi(); // به‌درستی اجرا می‌شود

function sayHi() {
  console.log("سلام");
}

Hoisting در var

متغیرهای تعریف‌شده با var نیز Hoist می‌شوند، اما فقط اعلان آن‌ها، نه مقداردهی‌شان:

Hoisting در var فقط اعلان را شامل می‌شود
خروجی کنسول اینجا نمایش داده می‌شود...

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

خروجی خط اول undefined است، نه خطا. دلیل این است که جاوااسکریپت پیش از اجرا، اعلان var city را به بالای Scope منتقل می‌کند، اما مقداردهی (= "تهران") دقیقاً همان‌جایی که در کد نوشته شده باقی می‌ماند. در نتیجه، کد بالا از دید جاوااسکریپت عملاً این‌طور اجرا می‌شود:

var city; // اعلان، به بالا منتقل شده
console.log(city); // undefined
city = "تهران"; // مقداردهی، سر جای اصلی خودش باقی مانده
console.log(city); // تهران

این دقیقاً همان رفتاری است که در فصل متغیرها اشاره کردیم اما توضیح کامل آن را به این فصل موکول کردیم.

Temporal Dead Zone چیست؟

حالا سؤال مهم این است: متغیرهای let و const چطور؟ آیا آن‌ها اصلاً Hoist نمی‌شوند؟ پاسخ کمی ظریف‌تر از یک بله یا خیر ساده است: این متغیرها هم Hoist می‌شوند، اما برخلاف var، پیش از رسیدن خط اجرا به محل تعریفشان، در ناحیه‌ای قرار می‌گیرند که به آن Temporal Dead Zone یا به‌اختصار TDZ (ناحیه‌ی مرده‌ی زمانی) گفته می‌شود؛ ناحیه‌ای که در آن، متغیر وجود دارد اما هرگونه تلاش برای خواندن یا استفاده از آن، خطا تولید می‌کند.

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

تحلیل کد

این کد با خطای ReferenceError: Cannot access 'age' before initialization متوقف می‌شود. توجه کنید که این پیام خطا با پیام خطای متغیر کاملاً تعریف‌نشده (مثل چیزی که در درس Scope Chain دیدیم) متفاوت است؛ آن‌جا پیام is not defined بود، اینجا پیام Cannot access ... before initialization است. این تفاوت دقیقاً نشان می‌دهد که age واقعاً وجود دارد (چون Hoist شده)، اما هنوز در TDZ قرار دارد و قابل استفاده نیست.

مرز TDZ دقیقاً کجاست؟

TDZ از ابتدای Scope شروع می‌شود و دقیقاً تا خط تعریف متغیر ادامه دارد؛ به‌محض رسیدن اجرای کد به همان خط تعریف، متغیر از TDZ خارج شده و قابل استفاده می‌شود:

{
  // از اینجا TDZ برای score شروع می‌شود
  // console.log(score); // اگر این خط از کامنت خارج شود، خطا می‌دهد

  let score = 100; // TDZ در همین‌جا پایان می‌یابد

  console.log(score); // 100 — قابل دسترسی است
}

چرا TDZ وجود دارد؟

TDZ صرفاً یک محدودیت بی‌دلیل نیست؛ هدف آن جلوگیری از باگ‌های پنهانی است که Hoisting در var ایجاد می‌کرد. در var، خواندن یک متغیر پیش از مقداردهی، بی‌سروصدا مقدار undefined برمی‌گرداند و ممکن است هیچ‌وقت متوجه اشتباه خود نشوید. اما let و const با ایجاد TDZ، این خطا را بلافاصله و به‌صورت صریح گزارش می‌دهند؛ به این ترتیب برنامه‌نویس مجبور می‌شود متغیرها را پیش از استفاده، به‌درستی تعریف کند.

Hoisting در const

رفتار const از نظر Hoisting و TDZ دقیقاً مشابه let است؛ تنها تفاوت آن‌ها، همان‌طور که در فصل متغیرها دیدیم، در قابلیت Re-assignment است، نه در Hoisting:

console.log(pi); // خطا: Cannot access 'pi' before initialization
const pi = 3.14;

جدول مقایسه‌ی Hoisting

نوعHoist می‌شود؟رفتار پیش از تعریف
Function Declarationبله (کامل)قابل فراخوانی است
varبله (فقط اعلان)undefined
letبله (اما در TDZ)خطا: Cannot access before initialization
constبله (اما در TDZ)خطا: Cannot access before initialization

Best Practices

  • همیشه متغیرها را در ابتدای Scope خودشان تعریف کنید تا هرگز درگیر TDZ نشوید و کد خواناتر باشد.
  • هرگز به رفتار Hoisting در var برای نوشتن کد تکیه نکنید؛ این رفتار بیشتر باید برای فهم کدهای قدیمی شناخته شود، نه برای نوشتن کد جدید.

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

Hoisting و TDZ نتیجه‌ی مستقیم نحوه‌ی ساخت Execution Context توسط جاوااسکریپت پیش از اجرای کد هستند؛ در درس بعدی، دقیقاً بررسی می‌کنیم که این فرایند در پشت‌صحنه چگونه اتفاق می‌افتد و چرا اصلاً چنین رفتاری در جاوااسکریپت وجود دارد.

جمع‌بندی

Hoisting به این معناست که جاوااسکریپت پیش از اجرای کد، از وجود اعلان متغیرها و توابع آگاه می‌شود. Function Declaration به‌طور کامل Hoist می‌شود و پیش از تعریف هم قابل فراخوانی است. متغیرهای var فقط از نظر اعلان Hoist می‌شوند و پیش از مقداردهی، مقدار undefined دارند. متغیرهای let و const نیز Hoist می‌شوند، اما تا رسیدن اجرای کد به خط تعریفشان، در Temporal Dead Zone قرار دارند و هرگونه دسترسی به آن‌ها در این بازه، خطا تولید می‌کند.