Scope، Hoisting و Closure
Scope، Hoisting و Closure؛ آموزش JavaScript به زبان ساده، همراه با مثال و خروجی.
Scope (محدودهی دید متغیر)
انواع
const global = "سراسری"; // Global scope
function fn() {
const local = "محلی"; // Function scope
if (true) {
let block = "بلوک"; // Block scope (let/const)
var leaky = "نشت میکنه"; // var به بلوک محدود نیست
}
// console.log(block); // ❌ ReferenceError
console.log(leaky); // ✅ → "نشت میکنه"
}
Scope Chain
وقتی متغیری پیدا نشه، JS به scope بیرونیتر میره تا به global برسه:
const a = 1;
function outer() {
const b = 2;
function inner() {
const c = 3;
return a + b + c; // inner → outer → global
}
return inner();
}
outer(); // → 6
Shadowing
const x = "بیرونی";
function f() {
const x = "درونی"; // x بیرونی رو میپوشونه
return x;
}
f(); // → "درونی"
Lexical Scope
scope بر اساس محل نوشتن کد تعیین میشه، نه محل صدا زدن:
const v = "global";
function show() { return v; }
function wrapper() {
const v = "local";
return show();
}
wrapper(); // → "global"
Hoisting
مفسر قبل از اجرا، تعریفها رو به بالای scope میکشه.
var
console.log(a); // → undefined (نه خطا)
var a = 5;
// معادل: var a; console.log(a); a = 5;
let / const و TDZ
// console.log(b); // ❌ ReferenceError: Cannot access 'b' before initialization
let b = 5;
فاصلهی ابتدای scope تا خط تعریف = Temporal Dead Zone (TDZ).
توابع
hello(); // ✅ کار میکنه
function hello() { console.log("hi"); }
// bye(); // ❌ TypeError: bye is not a function (var → undefined)
var bye = function () {};
// arrow(); // ❌ ReferenceError (const در TDZ)
const arrow = () => {};
class هم TDZ داره
// new A(); // ❌ ReferenceError
class A {}
اولویت: تابع بالاتر از var
console.log(typeof foo); // → "function"
var foo = 1;
function foo() {}
Closure
Closure یعنی تابع، متغیرهای scopeی که در آن ساخته شده رو حتی بعد از پایان اون scope به یاد میداره.
مثال پایه
function makeCounter() {
let count = 0; // متغیر خصوصی
return function () {
count++;
return count;
};
}
const c1 = makeCounter();
c1(); // → 1
c1(); // → 2
const c2 = makeCounter(); // شمارندهی مستقل
c2(); // → 1
کاربرد ۱: دادهی خصوصی (Encapsulation)
function createAccount(balance) {
return {
deposit(n) { balance += n; return balance; },
withdraw(n) {
if (n > balance) throw new Error("موجودی کافی نیست");
balance -= n;
return balance;
},
get balance() { return balance; },
};
}
const acc = createAccount(100);
acc.deposit(50); // → 150
acc.balance; // → 150
// acc.balance = 0 → تأثیری نداره؛ به متغیر داخلی دسترسی مستقیم نیست
کاربرد ۲: Function Factory
const power = (exp) => (base) => base ** exp;
const square = power(2);
const cube = power(3);
square(4); // → 16
cube(2); // → 8
کاربرد ۳: ماژول (الگوی قدیمی)
const counterModule = (function () {
let n = 0;
return {
inc: () => ++n,
reset: () => (n = 0),
};
})();
کاربرد ۴: Memoization
function memo(fn) {
const cache = new Map();
return (arg) => {
if (!cache.has(arg)) cache.set(arg, fn(arg));
return cache.get(arg);
};
}
دام معروف: closure داخل حلقه
// ❌ با var همه به یک i اشاره میکنن
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 0); // → 3, 3, 3
}
// ✅ راهحل ۱: let (برای هر دور یک i جدا)
for (let j = 0; j < 3; j++) {
setTimeout(() => console.log(j), 0); // → 0, 1, 2
}
// ✅ راهحل ۲: IIFE
for (var k = 0; k < 3; k++) {
((n) => setTimeout(() => console.log(n), 0))(k);
}
Closure و حافظه
تا وقتی تابعی که closure داره زندهست، متغیرهای گرفتهشده هم زنده میمونن. اگه دادهی بزرگی رو ناخواسته نگه میداره:
function leak() {
const big = new Array(1e6).fill("x");
return () => big.length; // big تا زنده بودن این تابع آزاد نمیشه
}
بعد از استفاده ارجاعها رو null کن یا از WeakRef استفاده کن.
تمرین
- تابع
once(fn)بنویس که فقط بار اول اجرا بشه و بعد همون نتیجه رو برگردونه. - یک
createCounterبا متدهایinc,dec,valueبنویس بدون اینکهcountاز بیرون قابل دسترس باشه. - توضیح بده چرا خروجی
for (var i...)باsetTimeoutهمیشه یک عدد تکراریه.