محدوده متغیرها و اجرای کد
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 Scope: کل برنامه
- Function Scope: داخل هر تابع
- Block 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 — امن
}
Execution Context — زمینه اجرا
موتور جاوا اسکریپت کد را در زمینههای اجرا (Execution Context) اجرا میکند. هر زمینه دو فاز دارد:
- فاز ایجاد (Creation): متغیرها و توابع ثبت میشوند، Scope Chain و this تعیین میشود
- فاز اجرا (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 دارد — ساختاری که متغیرهای محیط را نگه میدارد:
- Environment Record: جدولی از نامها و مقادیر متغیرهای این محدوده
- ارجاع به محیط بیرونی (outer): برای پیمایش Scope Chain
// مفهومی — چگونگی نگهداری متغیرها:
// 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
جاوا اسکریپت حافظه را خودکار مدیریت میکند:
- تخصیص (Allocation): هنگام ایجاد متغیرها و اشیا
- استفاده: خواندن و نوشتن
- آزادسازی (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 است:
- Mark: از ریشهها (متغیرهای سراسری، پشته فراخوانی) شروع و همه قابلدسترسیها علامتگذاری میشوند
- 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 پاک میشود — بدون نشتی!
بهترین شیوهها
- همیشه
const→ بعدlet→ هرگزvarجدید - متغیرها را در کوچکترین محدوده ممکن تعریف کنید
- تایمرها و event listenerهای بیاستفاده را پاک کنید
- از ابزار DevTools → Memory برای پروفایل حافظه استفاده کنید
- مراجع به DOM حذفشده را null کنید
جمعبندی
در این درس عمیقترین مفاهیم زیربنایی جاوا اسکریپت را آموختید: سه نوع Scope (Global، Function و Block)، زنجیره محدوده و جستوجوی متغیر، Lexical Scope بر اساس جای تعریف، Hoisting و TDZ، زمینه اجرا و پشته فراخوانی، و در نهایت مدیریت حافظه و Garbage Collection با الگوریتم Mark-and-Sweep. این مفاهیم پایه درک Closure، ماژولها و بهینهسازی کد در مباحث بعدیاند.
این مبحث بخشی از دوره آموزش کامل جاوا اسکریپت است.