Most people begin JavaScript by building things. You wire up a button, fetch some data, and watch the DOM change. Then the abstractions leak. Bugs surface that make no sense: variables exist before their declarations, functions remember variables they should not have access to, and the this keyword points at the window, a button, or nothing at all. That is usually the moment you realize you need to look under the hood and understand what the engine is actually doing.

Execution Context: The Two-Phase Setup

When the JavaScript engine in your browser or Node.js encounters a script, it does not simply read the file from top to bottom like a person scanning a page. Instead, it builds an execution context, a container that holds everything needed to run a particular chunk of code. Every execution context moves through two distinct phases.

Memory Creation Phase. During this initial pass, the engine scans the entire scope and allocates memory for every variable and function declaration it finds. If it sees a var, it reserves space and stores undefined as a placeholder. If it sees a function declaration, it stores the complete function body. This is why a function declared with the traditional function keyword can be called from lines that appear earlier in the same scope. The engine already knows about it before it starts executing.

Code Execution Phase. Now the engine runs your code line by line. Assignments happen here. Expressions are evaluated. Functions are invoked. If you wrote var name = "Alice";, the placeholder from phase one finally gets replaced with the string. Understanding this two-pass behavior clears up a surprising amount of confusion. The engine is not sloppy; it is following a strict setup routine.

Variables and the Temporal Dead Zone

Choosing between var, let, and const is not just a stylistic preference. var is function-scoped, which means it ignores braces entirely. Declare it inside an if block, and it leaks outward. That behavior might have made sense in early JavaScript, but in modern applications it causes real maintenance headaches. let and const are block-scoped. They respect curly braces and disappear when the block ends.

There is also a subtle but crucial difference in how hoisting treats them. var declarations are hoisted and immediately initialized with undefined. let and const are technically hoisted too; the engine knows they exist before it reaches the declaration line. But they are not initialized. They sit in a limbo called the temporal dead zone. If you try to read them before the declaration line executes, you get a hard ReferenceError rather than a sneaky undefined. That crash is actually helpful. It prevents logic from proceeding on top of uninitialized data.

Lexical Scope and Closures

Scope answers a simple question: where can I access this variable? JavaScript uses lexical scope, which means a function's access rights are decided by where it was physically written in the source code, not by where it ends up being called. If you define a function inside another function, the inner one can reach outward to read variables from its parent. The outer function cannot reach inward. This relationship is static. You could pass that inner function across modules, store it in a global variable, and call it from an entirely different file. It still remembers the variables from the scope where it was born.

That behavior naturally produces closures. A closure forms when an inner function keeps a reference to a variable from its outer scope. Even after the outer function finishes executing and its local variables should have been garbage collected, JavaScript preserves them in memory because the inner function still needs them. The inner function carries its surrounding environment with it.

This is not merely an academic detail. Closures give you a practical way to create private state in a language that lacks explicit access modifiers.

function makeCounter() {
  let count = 0;
  return function() {
    count = count + 1;
    return count;
  };
}

const counter = makeCounter();
console.log(counter()); // 1
console.log(counter()); // 2

Here, count is hidden. Nothing outside the returned function can reset it or read it directly. That is a private variable built from scope mechanics alone.

Hoisting in Practice

معمول است که بشنوید جاوااسکریپت «اعلان‌ها را به بالا منتقل می‌کند». این یک مدل ذهنی مفید است، اما کد در واقع بازنویسی نمی‌شود. در طول مرحله ایجاد حافظه، موتور صرفاً اعلان‌ها را قبل از شروع اجرا ثبت می‌کند. عبارتی مانند var x = 5; به‌گونه‌ای رفتار می‌کند که گویی اعلان و مقداردهی از هم جدا شده‌اند. اعلان var x; زودتر پردازش شده و با مقدار undefined مقداردهی می‌شود. مقداردهی x = 5; دقیقاً در همان جایی که نوشته‌اید باقی می‌ماند و در مرحله اجرا اجرا می‌شود.

به همین دلیل، var می‌تواند منجر به نتایج غافلگیرکننده‌ای شود. متغیری که در نزدیکی بالای یک تابع استفاده می‌شود، ممکن است undefined باشد، حتی اگر یک مقداردهی بزرگ در پایین تابع قرار داشته باشد. استفاده از let و const آن تله‌ی خاص را از بین می‌برد، زیرا منطقه مرده زمانی (temporal dead zone) شما را مجبور می‌کند که اعلان‌های خود را بالاتر از محل استفاده قرار دهید.

چگونه this مقدار خود را می‌گیرد

اگر زمینه اجرا (execution context) و محدوده (scope) تعیین کنند که متغیرها کجا قرار می‌گیرند، this تعیین می‌کند که در حال حاضر کدام شیء مسئول است. برخلاف متغیرهای لکسیکال، this بر اساس محل نوشته شدن یک تابع تعیین نمی‌شود، بلکه کاملاً بر اساس نحوه‌ی فراخوانی تابع تعیین می‌شود.

اتصال پیش‌فرض (Default binding) زمانی رخ می‌دهد که شما یک تابع ساده و مستقل را فراخوانی می‌کنید. در حالت غیر سخت‌گیرانه (non-strict mode)، this به شیء سراسری (global object) بازمی‌گردد. در مرورگر، این شیء همان window است. اگر تابعی را بدون هیچ زمینه‌ای فراخوانی کنید، ممکن است ناخواسته و بدون اینکه متوجه شوید، با وضعیت سراسری (global state) درگیر شوید.

اتصال ضمنی (Implicit binding) زمانی اتفاق می‌افتد که شما یک تابع را به عنوان یک متد روی یک شیء فراخوانی می‌کنید. اگر بنویسید user.sayName()، نقطه به‌طور بی‌صدا به موتور می‌گوید که this را برای مدت آن فراخوانی، برابر با user قرار دهد. محل فراخوانی مهم‌تر از محل تعریف تابع است.

اتصال صریح (Explicit binding) به شما اجازه می‌دهد همه چیز را به‌صورت دستی بازنویسی کنید. متدهای call() و apply() یک تابع را بلافاصله فراخوانی می‌کنند و در عین حال this را مجبور می‌کنند که یک شیء مشخص باشد که شما ارائه می‌دهید. تنها تفاوت آن‌ها در نحوه‌ی ارسال آرگومان‌ها است: call لیستی که با کاما از هم جدا شده‌اند را می‌گیرد، در حالی که apply یک آرایه دریافت می‌کند. bind() متفاوت عمل می‌کند. این متد تابع را بلافاصله فراخوانی نمی‌کند، بلکه تابع جدیدی را برمی‌گرداند که this در آن به‌طور دائمی روی مقداری که ارائه کرده‌اید قفل شده است. این قابلیت برای callbackهایی که ممکن است در صورت انتقال، زمینه (context) خود را از دست بدهند، بسیار ارزشمند است.

اتصال با new (New binding) زمانی وارد عمل می‌شود که از کلمه کلیدی new در مقابل فراخوانی یک تابع استفاده می‌کنید. موتور یک شیء خالی کاملاً جدید می‌سازد، پیوند پروتوتایپ آن را تنظیم می‌کند و this را در داخل سازنده (constructor) به آن نمونه‌ی تازه اشاره می‌دهد.

نوشتن کدی که ماندگار باشد

درک مکانیسم‌ها تنها نیمی از هنر است. نیمه‌ی دیگر، نوشتن کدی است که انسان‌ها بتوانند شش ماه دیگر آن را بخوانند.

DRY (خودتان را تکرار نکنید) بدیهی به نظر می‌رسد، اما مدام نقض می‌شود. اگر متوجه شدید که در حال نوشتن یک منطق اعتبارسنجی یا الگوی فراخوانی API یکسان در سه فایل مختلف هستید، آن را استخراج کنید. یک تابع بنویسید. داشتن یک منبع حقیقت (source of truth) به معنای داشتن یک مکان واحد برای به‌روزرسانی در هنگام تغییر الزامات است و این کار باعث صرفه‌جویی در زمانی می‌شود که توصیف دشواری از آن وجود دارد.

KISS (ساده نگهش دار، احمق!) دفاعی در برابر غرور است. عملگرهای شرطی سه تایی (ternaries) تو در تو و کلوژرهای تک‌خطی هوشمندانه به نظر می‌رسند، اما هنگام عیب‌یابی (debugging) ساعت‌ها وقت می‌گیرند. الگوی کلوژری که قبلاً نشان دادم قدرتمند است، اما تو در تو کردن پنج سطح فقط به این دلیل که می‌توانید، یک اشتباه است. کد ساده در برابر تغییرات تیم، حوادث محیط عملیاتی (production incidents) و آن هشدارهای نیمه‌شب که وقتی چیزی خراب می‌شود و هیچ‌کس دلیلش را به یاد نمی‌آورد، دوام می‌آورد.

پاداش واقعی

مطالعه‌ی اجرا