جاوااسکریپت چطور حافظه را مدیریت می‌کند؟ مکانیزم Hoisting، Temporal Dead Zone و فازهای اجرای کد

اگه تا حالا با یه ارور عجیب و غریب مثل Cannot access 'x' before initialization روبه‌رو شدی و نفهمیدی چرا جاوااسکریپت داره اذیتت می‌کنه، بدون که تنها نیستی. این ارور و کلی رفتار عجیب دیگه‌ی جاوااسکریپت، ریشه‌شون توی یه سری مکانیزم داخلی به اسم Hoisting و Temporal Dead Zone (TDZ) هست. توی این مقاله می‌خوایم از صفر بریم زیر پوست جاوااسکریپت و ببینیم واقعاً موقع اجرای کد، توی حافظه چه خبره.

قبل از هر چیزی: حافظه توی جاوااسکریپت چطور سازمان‌دهی می‌شه؟

موتور جاوااسکریپت (مثل V8 توی کروم و نود) برای مدیریت حافظه از دو تا فضای اصلی استفاده می‌کنه: Stack (پشته) و Heap (توده). مقادیر primitive (مثل عدد، رشته، بولین) و رفرنس‌ها توی Stack ذخیره می‌شن چون سایزشون مشخصه و قابل پیش‌بینیه. اما شیء‌ها، آرایه‌ها و فانکشن‌ها توی Heap ذخیره می‌شن چون سایزشون می‌تونه دینامیک تغییر کنه.

Stack و Heap توی موتور جاوااسکریپت Stack age = 25 name → ref#1 user → ref#2 Heap "Ali" (ref#1) { id: 1, ...} (ref#2)

نکته‌ی مهم اینه که Stack سریع و منظمه، اما محدود؛ Heap بزرگ‌تره ولی کندتر و بی‌نظم‌تر. این تفاوت دقیقاً پایه‌ی خیلی از رفتارهایی هست که الان می‌خوایم بررسی کنیم.

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

هر بار که یه اسکریپت یا یه فانکشن اجرا می‌شه، موتور جاوااسکریپت یه Execution Context می‌سازه و اون رو توی دو فاز پردازش می‌کنه:

  • Creation Phase (فاز ساخت): موتور قبل از اجرای هر خطی از کد، یه دور کل کد رو می‌خونه و برای همه‌ی متغیرها و فانکشن‌ها توی حافظه جا رزرو می‌کنه.
  • Execution Phase (فاز اجرا): حالا کد خط به خط واقعاً اجرا می‌شه و مقادیر واقعی به متغیرها نسبت داده می‌شه.
دو فاز اجرای کد در جاوااسکریپت 1. Creation Phase جا رزرو کردن حافظه برای var, let, const, function var → undefined let/const → TDZ 2. Execution Phase اجرای واقعی خط به خط کد مقداردهی واقعی به متغیرها خروج متغیر از TDZ

دقیقاً همین «رزرو کردن حافظه قبل از اجرا» همون چیزیه که باعث می‌شه پدیده‌ی Hoisting اتفاق بیفته.

Hoisting دقیقاً چیه؟

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

بیایم با یه مثال ساده شروع کنیم:

console.log(myVar); // undefined (نه ارور!)
var myVar = "سلام";
console.log(myVar); // سلام

اینجا موتور توی فاز Creation، متغیر myVar رو شناسایی کرده و از همون اول با مقدار undefined توی حافظه جا داده. برای همین خط اول ارور نمی‌ده و undefined چاپ می‌شه.

حالا فانکشن‌ها رو ببینیم. فانکشن‌هایی که با function declaration نوشته می‌شن، کاملاً hoist می‌شن (هم اسم، هم بدنه):

sayHi(); // کار می‌کنه!

function sayHi() {
  console.log("سلام از تابع Hoist شده");
}

اما اگه فانکشن رو به‌صورت expression (نسبت‌دادن به یه متغیر) بنویسی، فقط خود متغیر hoist می‌شه، نه مقدارش:

sayHi(); // TypeError: sayHi is not a function

var sayHi = function () {
  console.log("این کار نمی‌کنه");
};

حالا برسیم به Temporal Dead Zone (TDZ)

اینجا داستان یه‌کم فرق می‌کنه. برخلاف var، متغیرهای let و const هم توی فاز Creation شناسایی و hoist می‌شن، اما موتور جاوااسکریپت بهشون مقدار undefined نمی‌ده. اونا توی یه ناحیه‌ی «مرده» به اسم Temporal Dead Zone قرار می‌گیرن؛ یعنی از لحظه‌ای که بلاک شروع می‌شه تا لحظه‌ای که واقعاً به اون متغیر مقدار داده بشه، هر تلاشی برای دسترسی بهشون با ReferenceError روبه‌رو می‌شه.

console.log(city); // ReferenceError: Cannot access 'city' before initialization
let city = "تهران";
تایم‌لاین Temporal Dead Zone برای let/const TDZ (دسترسی = ReferenceError) قابل دسترسی شروع بلاک { let city = "تهران" پایان بلاک }

نکته‌ی جالب اینه که typeof هم نمی‌تونه TDZ رو دور بزنه. توی حالت عادی typeof روی یه متغیر تعریف‌نشده ارور نمی‌ده، ولی اگه اون متغیر توی TDZ باشه، باز هم ارور می‌گیری:

console.log(typeof notDeclared); // "undefined" — مشکلی نیست

console.log(typeof inTDZ); // ReferenceError!
let inTDZ = 10;

مقایسه‌ی مستقیم: var در برابر let/const

فرق اصلی این دو تا دقیقاً همینجاست که چه موقع «قابل دسترسی» می‌شن:

  • var → توی فاز Creation هم hoist می‌شه و هم مستقیم با undefined initialize می‌شه؛ یعنی TDZ عملاً نداره (یا خیلی کوتاهه).
  • let و const → توی فاز Creation فقط hoist می‌شن ولی initialize نمی‌شن؛ تا رسیدن به خط تعریف واقعی، توی TDZ گیر می‌مونن.
// نسخه‌ی var
{
  console.log(a); // undefined
  var a = 1;
}

// نسخه‌ی let
{
  console.log(b); // ReferenceError
  let b = 1;
}

چرا var کم‌کم کنار گذاشته شد؟

دلیل اصلیش همین رفتار «سکوت‌آمیز» var هست. وقتی یه متغیر بدون خطا مقدار undefined برمی‌گردونه، خیلی راحت باگ‌های ظریف و سخت‌رصد ایجاد می‌شه، چون هیچ هشداری نمی‌گیری. یه مشکل دیگه‌ی var اینه که function-scoped هست، نه block-scoped؛ یعنی داخل یه if یا یه حلقه هم که تعریفش کنی، بیرون از اون بلاک هم در دسترسه:

if (true) {
  var x = 5;
}
console.log(x); // 5 — نشتی از بلاک بیرون اومد!

if (true) {
  let y = 5;
}
console.log(y); // ReferenceError — y درست داخل بلاک محبوس مونده

let و const با block scope و TDZ، این نشتی رو می‌بندن و مجبورت می‌کنن متغیر رو دقیقاً همون‌جایی که باید تعریفش کنی، تعریف کنی. یه مثال کلاسیک دیگه که خیلی‌ها باهاش دردسر داشتن، رفتار var توی حلقه‌ها با setTimeout بود:

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 0);
}
// خروجی: 3, 3, 3  (چون i مشترکه بین همه تکرارها)

for (let j = 0; j < 3; j++) {
  setTimeout(() => console.log(j), 0);
}
// خروجی: 0, 1, 2  (چون هر تکرار let خودش رو داره)

یه جمع‌بندی از کل ماجرا با یه مثال کامل

console.log(typeof fn); // "function"
console.log(typeof v);  // "undefined"
console.log(typeof l);  // ReferenceError! (TDZ)

var v = 10;
let l = 20;

function fn() {
  return "من کامل hoist شدم";
}

دلیل هر خط اینه: fn کامل hoist شده (تابع declaration)، v فقط با undefined hoist شده، و l توی TDZ گیر کرده چون هنوز به خط let l = 20 نرسیدیم.

چند نکته‌ی کاربردی برای کدنویسی تمیزتر

  • همیشه از let و const استفاده کن، نه var — مگر توی کد قدیمی که مجبوری.
  • متغیرها رو همون بالای بلاکی که ازشون استفاده می‌کنی تعریف کن، نه وسط یا آخر بلاک؛ اینطوری هیچ‌وقت وارد TDZ نمی‌شی.
  • اگه یه function declaration داری، صداکردنش قبل از تعریف مشکلی نداره؛ ولی این کار رو عادت نکن چون خوانایی کد رو کم می‌کنه.
  • مراقب shadowing باش: تعریف یه متغیر با همون اسم توی یه اسکوپ تودرتو، می‌تونه باعث بشه ناخواسته وارد TDZ همون متغیر داخلی بشی، حتی اگه یه نسخه‌ی بیرونی‌ش از قبل مقدار داشته باشه.

جمع‌بندی نهایی

Hoisting و TDZ دو تا مکانیزم داخلی هستن که مستقیم از فاز Creation موتور جاوااسکریپت میان بیرون. Hoisting یعنی اعلان‌ها زودتر از بقیه‌ی کد پردازش می‌شن، و TDZ یعنی برای let/const، بین اعلان و مقداردهی، یه بازه‌ی «ممنوعه» وجود داره. فهمیدن این دو مفهوم کمک می‌کنه ارورهای عجیب رو سریع‌تر بفهمی، و مهم‌تر از اون، کدی بنویسی که از اول کمتر توی این تله‌ها گیر بیفته.

منابع