مقدمه
دو سوال رایج در مصاحبههای فنی جاوااسکریپت:
«Scope چیست و چه انواعی دارد؟»
«Closure چیست؟ یک مثال واقعی بزنید.»
این دو مفهوم نه فقط برای مصاحبه، بلکه برای درک رفتار واقعی کد جاوااسکریپت ضروری هستند. وقتی میبینید یک تابع به متغیری دسترسی دارد که «نباید» داشته باشد، یا وقتی یک متغیر ناخواسته global میشود — همیشه پشتش Scope و Closure هست.
// این کد چه چاپ میکند؟
function makeCounter() {
let count = 0;
return function() {
count++;
return count;
};
}
const counter = makeCounter();
console.log(counter()); // ؟
console.log(counter()); // ؟
console.log(counter()); // ؟
اگر جواب را نمیدانید — این مقاله دقیقاً برای شماست. اگر میدانید — بخشهای پیشرفتهتر را بخوانید تا عمیقتر شوید.
Scope چیست؟
Scope (محدوده یا دامنه) قانونی است که تعیین میکند هر متغیر در کجای کد قابل دسترسی است و در کجا وجود ندارد.
به زبان ساده: Scope جواب این سوال را میدهد که «آیا این متغیر اینجا وجود دارد؟»
let globalVar = 'من global هستم';
function myFunction() {
let localVar = 'من local هستم';
console.log(globalVar); // ✅ قابل دسترسی
console.log(localVar); // ✅ قابل دسترسی
}
myFunction();
console.log(globalVar); // ✅ قابل دسترسی
console.log(localVar); // ❌ ReferenceError: localVar is not defined
در این مثال، localVar فقط داخل myFunction وجود دارد — خارج از آن، انگار اصلاً تعریف نشده.
انواع Scope در جاوااسکریپت
جاوااسکریپت چهار نوع Scope دارد که هر کدام رفتار متفاوتی دارند.
۱. Global Scope (محدوده سراسری)
متغیرهایی که خارج از هر تابع یا بلاک تعریف میشوند، Global Scope دارند و از هر جایی در کد قابل دسترسی هستند.
// Global Scope
let siteName = 'codeloop.ir';
const version = '2.0';
var legacyVar = 'قدیمی';
function showSite() {
console.log(siteName); // ✅ از داخل تابع قابل دسترسی
}
function updateSite() {
siteName = 'codeloop.ir v2'; // ✅ میتوان تغییر داد
}
showSite(); // 'codeloop.ir'
updateSite();
showSite(); // 'codeloop.ir v2'
// ⚠️ var در Global Scope به window اضافه میشود
console.log(window.legacyVar); // 'قدیمی'
console.log(window.siteName); // undefined — let و const اضافه نمیشوند
هشدار مهم: استفاده بیش از حد از Global Variables از رایجترین اشتباهات مبتدیان است. هر تابعی میتواند آنها را تغییر دهد و ردیابی باگ سخت میشود. در پروژههای بزرگ، Global Variable های زیاد به «آلودگی namespace» منجر میشود.
۲. Function Scope (محدوده تابع)
متغیرهایی که داخل تابع تعریف میشوند، فقط داخل همان تابع قابل دسترسی هستند. هر بار که تابع اجرا میشود، یک Scope جدید و مستقل ساخته میشود.
function calculateDiscount(price, rate) {
let discountAmount = price * rate; // Function Scope
let finalPrice = price - discountAmount;
return finalPrice;
}
console.log(calculateDiscount(100000, 0.2)); // 80000
console.log(discountAmount); // ❌ ReferenceError
// هر بار که تابع اجرا میشود، scope جدید و مستقل ساخته میشود
function greet(name) {
let message = 'سلام ' + name; // هر بار یک message جداگانه
return message;
}
greet('رضا'); // scope اول — message = 'سلام رضا'
greet('علی'); // scope دوم — message = 'سلام علی'
// دو scope کاملاً مستقل از هم — هیچ اشتراکی ندارند
۳. Block Scope (محدوده بلاک)
با معرفی let و const در ES6، متغیرها میتوانند Block Scope داشته باشند — یعنی محدود به یک جفت آکولاد {}.
// Block Scope با let و const
if (true) {
let blockLet = 'من Block Scope دارم';
const blockConst = 'من هم Block Scope دارم';
var functionVar = 'من Function Scope دارم!';
console.log(blockLet); // ✅
console.log(blockConst); // ✅
}
console.log(functionVar); // ✅ var از بلاک خارج شد!
console.log(blockLet); // ❌ ReferenceError
console.log(blockConst); // ❌ ReferenceError
// تفاوت مهم در حلقهها — یکی از رایجترین سوالات مصاحبه
for (var i = 0; i < 3; i++) { /* کد */ }
console.log(i); // 3 — var از حلقه خارج شد!
for (let j = 0; j < 3; j++) { /* کد */ }
console.log(j); // ❌ ReferenceError — let محدود به بلاک است
// در switch هم اعمال میشود
switch (action) {
case 'READ': {
const permission = 'read-only'; // Block Scope داخل {}
console.log(permission);
break;
}
case 'WRITE': {
const permission = 'read-write'; // ✅ تعارض نیست — scope جداست
console.log(permission);
break;
}
}
۴. Lexical Scope (محدوده لکسیکال)
این مهمترین نوع Scope برای درک Closure است. جاوااسکریپت از Lexical Scope (یا Static Scope) استفاده میکند — یعنی Scope یک تابع بر اساس جایی که نوشته شده تعیین میشود، نه جایی که اجرا میشود.
let language = 'JavaScript';
function outer() {
let framework = 'React';
function inner() {
let library = 'Lodash';
// inner به همه چیز در scope های بالاتر دسترسی دارد
console.log(language); // ✅ 'JavaScript' — از Global
console.log(framework); // ✅ 'React' — از outer
console.log(library); // ✅ 'Lodash' — از خودش
}
inner();
console.log(library); // ❌ inner به outer دسترسی نمیدهد — یکطرفه است
}
outer();
console.log(framework); // ❌ خارج از outer
// مثال Lexical vs Dynamic Scope
let value = 'global';
function getValue() {
return value; // کدام value؟ — جایی که نوشته شده (global)
}
function changeContext() {
let value = 'local'; // این value
return getValue(); // ...تأثیری ندارد — getValue در global نوشته شده
}
console.log(changeContext()); // 'global' — نه 'local'!
Scope Chain — زنجیره جستجو
let a = 'global a';
function first() {
let b = 'first b';
function second() {
let c = 'second c';
function third() {
let d = 'third d';
// جستجوی 'b' در third:
// ۱. third scope → نیست
// ۲. second scope → نیست
// ۳. first scope → ✅ پیدا شد!
console.log(a); // ✅ از global
console.log(b); // ✅ از first
console.log(c); // ✅ از second
console.log(d); // ✅ از خودش
}
third();
}
second();
}
first();
// اگر متغیر در هیچ scope ای پیدا نشود:
console.log(xyz); // ❌ ReferenceError: xyz is not defined
// جاوااسکریپت تمام زنجیره را گشت و پیدا نکرد
مقایسه var، let و const در Scope
ویژگی | var | let | const |
|---|---|---|---|
Scope | Function | Block | Block |
Hoisting | بله (undefined) | بله (TDZ) | بله (TDZ) |
تعریف مجدد | ✅ مجاز | ❌ ممنوع | ❌ ممنوع |
تغییر مقدار | ✅ مجاز | ✅ مجاز | ❌ ممنوع |
به window اضافه میشود | ✅ بله | ❌ خیر | ❌ خیر |
توصیه در کد مدرن | ❌ استفاده نکنید | ✅ برای مقادیر متغیر | ✅ پیشفرض |
Closure چیست؟
حالا که Scope را فهمیدید، وقت Closure است.
Closure یعنی یک تابع داخلی به متغیرهای Scope بیرونیاش دسترسی دارد — حتی بعد از اینکه تابع بیرونی اجرا شده و تمام شده است.
به زبان ساده: تابع «محیط اطرافش در زمان تعریف» را به خاطر میسپارد.
function outer() {
let message = 'سلام از outer!';
function inner() {
// inner به message دسترسی دارد
console.log(message);
}
return inner; // تابع را برمیگرداند — اجرا نمیکند
}
const myFunc = outer(); // outer اجرا شد و از Stack حافظه حذف شد
myFunc(); // ✅ 'سلام از outer!' — هنوز به message دسترسی دارد!
چرا این کار میکند؟ چون وقتی inner ساخته شد، یک بسته (closure) روی محیط Lexical خود ایجاد کرد — شامل message. حتی وقتی outer تمام شد، JavaScript Engine آن محیط را در حافظه نگه میدارد چون inner هنوز به آن نیاز دارد.
جواب مثال اول — Counter
function makeCounter(startFrom = 0) {
let count = startFrom; // در Closure نگه داشته میشود
return {
increment() { return ++count; },
decrement() { return --count; },
reset() { count = startFrom; return count; },
getCount() { return count; },
};
}
const counter1 = makeCounter(0);
const counter2 = makeCounter(10); // counter مستقل از 10
counter1.increment(); // 1
counter1.increment(); // 2
counter1.increment(); // 3
counter2.increment(); // 11
counter2.increment(); // 12
console.log(counter1.getCount()); // 3 — مستقل از counter2
console.log(counter2.getCount()); // 12 — مستقل از counter1
// هر Closure محیط (environment) جداگانهای دارد!
کاربردهای واقعی Closure
۱. Private Variables
function createUserProfile(name, role) {
// Private — از بیرون قابل دسترسی نیستند
let _name = name;
let _role = role;
let _loginHistory = [];
return {
getName() { return _name; },
getRole() { return _role; },
login(timestamp = new Date()) {
_loginHistory.push(timestamp);
console.log(_name + ' وارد شد — ' + _loginHistory.length + ' بار');
},
getLoginHistory() {
return [..._loginHistory]; // کپی میدهد، نه reference مستقیم
},
promote(newRole) {
if (_role === 'user') {
_role = newRole;
console.log(_name + ' به ' + newRole + ' ارتقا یافت');
}
},
};
}
const user = createUserProfile('رضا', 'user');
user.login();
user.login();
user.promote('admin');
console.log(user.getName()); // 'رضا'
console.log(user.getLoginHistory()); // [Date, Date]
console.log(user._name); // undefined — private است!
۲. Function Factory
function createPriceConverter(exchangeRate, currencyName) {
return function(price) {
const converted = (price * exchangeRate).toLocaleString('fa');
return converted + ' ' + currencyName;
};
}
const toToman = createPriceConverter(60000, 'تومان');
const toEuro = createPriceConverter(0.011, '€');
const toDollar = createPriceConverter(0.012, '$');
console.log(toToman(10)); // '۶۰۰,۰۰۰ تومان'
console.log(toEuro(10)); // '0.11 €'
console.log(toDollar(10)); // '0.12 $'
// هر تابع exchangeRate و currencyName خودش را در Closure دارد
۳. Memoization
function memoize(fn) {
const cache = new Map(); // در Closure نگه داشته میشود
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
console.log('از کش: ' + key);
return cache.get(key);
}
const result = fn(...args);
cache.set(key, result);
return result;
};
}
function heavyCalculation(n) {
console.log('محاسبه برای ' + n + '...');
return n * n * n;
}
const fastCalc = memoize(heavyCalculation);
fastCalc(10); // محاسبه برای 10...
fastCalc(10); // از کش: [10] — فوری!
fastCalc(20); // محاسبه برای 20...
fastCalc(20); // از کش: [20] — فوری!
۴. Debounce — پرکاربردترین Closure در فرانتاند
function debounce(fn, delay) {
let timer = null; // در Closure نگه داشته میشود
return function(...args) {
clearTimeout(timer); // اگر قبلاً timer بود، کنسل کن
timer = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
// جستجوی زنده — بدون debounce با هر کیستروک درخواست ارسال میشد!
const searchHandler = debounce(async (query) => {
if (!query.trim()) return;
const response = await fetch('/api/search?q=' + query);
const results = await response.json();
console.log('نتایج:', results.length);
}, 300);
document.querySelector('#search').addEventListener('input', (e) => {
searchHandler(e.target.value);
// فقط ۳۰۰ms بعد از آخرین کیستروک، درخواست ارسال میشود
});
۵. Module Pattern
const ArticleModule = (function() {
// Private
let articles = [];
let nextId = 1;
let _totalViews = 0;
function _generateSlug(title) {
return title.toLowerCase().trim().replace(/s+/g, '-').replace(/[^w-ۿ-]/g, '');
}
function _validate(data) {
if (!data.title || data.title.length < 5) throw new Error('عنوان باید حداقل ۵ کاراکتر باشد');
if (!data.content) throw new Error('محتوا الزامی است');
}
// Public API
return {
add(data) {
_validate(data);
const article = {
id: nextId++,
slug: _generateSlug(data.title),
views: 0,
createdAt: new Date(),
...data,
};
articles.push(article);
return article;
},
getAll() { return [...articles]; },
getCount() { return articles.length; },
getTotalViews() { return _totalViews; },
view(id) {
const article = articles.find(a => a.id === id);
if (article) { article.views++; _totalViews++; }
},
findBySlug(slug) { return articles.find(a => a.slug === slug) || null; },
};
})();
ArticleModule.add({ title: 'اسکوپ در جاوااسکریپت', content: 'محتوا...' });
ArticleModule.add({ title: 'کلوژر چیست و چطور کار میکند', content: 'محتوا...' });
ArticleModule.view(1);
console.log(ArticleModule.getCount()); // 2
console.log(ArticleModule.getTotalViews()); // 1
console.log(articles); // ❌ ReferenceError — private است
مشکل کلاسیک var در حلقه
// ❌ مشکل — با var
const buttons = [];
for (var i = 0; i < 5; i++) {
buttons.push(function() { return i; });
}
console.log(buttons[0]()); // 5 — نه 0!
console.log(buttons[2]()); // 5 — نه 2!
// چون var یک متغیر مشترک دارد و وقتی تابع اجرا میشود، i = 5
// ✅ راهحل ۱ — با let (Block Scope)
const buttons2 = [];
for (let i = 0; i < 5; i++) {
buttons2.push(function() { return i; }); // هر iteration یک i جداگانه
}
console.log(buttons2[0]()); // 0 ✅
console.log(buttons2[2]()); // 2 ✅
// ✅ راهحل ۲ — با IIFE (روش قبل از ES6)
const buttons3 = [];
for (var i = 0; i < 5; i++) {
buttons3.push((function(j) {
return function() { return j; }; // j در Closure نگه داشته میشود
})(i));
}
console.log(buttons3[0]()); // 0 ✅
console.log(buttons3[2]()); // 2 ✅
Closure در React Hooks
// React Hooks کاملاً بر پایه Closure هستند
function Counter() {
const [count, setCount] = useState(0);
// هر render یک Closure جدید میسازد که مقدار count آن render را دارد
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
// ─── مشکل رایج: Stale Closure ───
function BadTimer() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(count + 1); // ❌ count همیشه 0 است!
// این Closure مقدار count در زمان ساخته شدنش را نگه میدارد
}, 1000);
return () => clearInterval(interval);
}, []); // [] → Closure یکبار ساخته میشود با count = 0
}
// ✅ راهحل — Functional Update
function GoodTimer() {
const [count, setCount] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setCount(prev => prev + 1); // ✅ مقدار فعلی را میگیرد
}, 1000);
return () => clearInterval(interval);
}, []);
}
// ✅ راهحل دوم — useRef
function GoodTimer2() {
const [count, setCount] = useState(0);
const countRef = useRef(count);
countRef.current = count;
useEffect(() => {
const interval = setInterval(() => {
setCount(countRef.current + 1); // ✅ همیشه آخرین مقدار
}, 1000);
return () => clearInterval(interval);
}, []);
}
💡 برای تمرین عملی
با استفاده از API رایگان فارسی کدلوپ Scope و Closure را در عمل تمرین کنید:
تمرین ۱ — API Client با Closure
function createApiClient(baseURL) {
let requestCount = 0;
const cache = new Map();
async function _fetch(endpoint) {
const url = baseURL + endpoint;
if (cache.has(url)) {
console.log('کش ✅ : ' + url);
return cache.get(url);
}
requestCount++;
console.log('درخواست ' + requestCount + ': ' + url);
const res = await fetch(url);
if (!res.ok) throw new Error('خطای HTTP: ' + res.status);
const data = await res.json();
cache.set(url, data);
return data;
}
return {
get: (endpoint) => _fetch(endpoint),
getStats() { return { totalRequests: requestCount, cachedUrls: cache.size }; },
clearCache() { cache.clear(); },
};
}
const api = createApiClient('https://freeapi.codeloop.ir');
async function runTest() {
const p1 = await api.get('/products');
const p2 = await api.get('/products'); // از کش
const u1 = await api.get('/users');
console.log('آمار:', api.getStats());
// { totalRequests: 2, cachedUrls: 2 }
}
runTest();
تمرین ۲ — Rate Limiter با Closure
function createRateLimiter(maxRequests, windowMs) {
const timestamps = []; // در Closure نگه داشته میشود
return async function limited(fn) {
const now = Date.now();
const cutoff = now - windowMs;
// حذف درخواستهای خارج از پنجره زمانی
while (timestamps.length > 0 && timestamps[0] < cutoff) {
timestamps.shift();
}
if (timestamps.length >= maxRequests) {
const wait = Math.ceil((timestamps[0] + windowMs - now) / 1000);
throw new Error('Rate limit! ' + wait + ' ثانیه صبر کنید');
}
timestamps.push(now);
return fn();
};
}
// حداکثر ۳ درخواست در هر ۱۰ ثانیه
const limited = createRateLimiter(3, 10000);
async function safeFetch(url) {
return limited(() => fetch(url).then(r => r.json()));
}
for (let i = 0; i < 5; i++) {
safeFetch('https://freeapi.codeloop.ir/products')
.then(d => console.log('موفق — ' + d.length + ' محصول'))
.catch(e => console.error('❌ ' + e.message));
}
تمرین ۳ — State Manager با Closure
function createStore(initialState) {
let state = { ...initialState };
const listeners = new Set();
return {
getState() {
return { ...state }; // کپی — از mutation جلوگیری میکند
},
setState(updates) {
const prevState = { ...state };
state = { ...state, ...updates };
// اطلاعرسانی به همه listener ها
listeners.forEach(listener => listener(state, prevState));
},
subscribe(listener) {
listeners.add(listener);
return () => listeners.delete(listener); // unsubscribe function
},
};
}
const store = createStore({ count: 0, user: null, loading: false });
// subscribe
const unsubscribe = store.subscribe((newState, prevState) => {
console.log('تغییر state:', prevState, '→', newState);
});
store.setState({ count: 1 });
store.setState({ user: { name: 'رضا' }, loading: true });
store.setState({ loading: false });
unsubscribe(); // دیگر اطلاعرسانی نمیشود
store.setState({ count: 999 }); // listener صدا زده نمیشود
اشتباهات رایج
/* ─── اشتباه ۱: var در حلقه ─── */
for (var i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 100); // ❌ همیشه 5
}
// ✅ با let
for (let i = 0; i < 5; i++) {
setTimeout(() => console.log(i), 100); // ✅ 0,1,2,3,4
}
/* ─── اشتباه ۲: Memory Leak ─── */
function createHandler() {
const hugeData = new Array(1000000).fill('x'); // ~8MB
return function() {
// hugeData هیچوقت استفاده نمیشود
// اما چون در Closure است، هیچوقت Garbage Collect نمیشود!
return 'done';
};
}
// ✅ فقط چیزی که نیاز دارید را نگه دارید
function createHandlerFixed() {
const hugeData = new Array(1000000).fill('x');
const dataLength = hugeData.length; // فقط این را نیاز داریم
// hugeData حالا میتواند Garbage Collect شود
return function() {
return 'تعداد: ' + dataLength;
};
}
/* ─── اشتباه ۳: Closure = Reference نه Copy ─── */
function createAdder() {
let total = 0;
return {
add(n) { total += n; },
getTotal(){ return total; },
};
}
const adder = createAdder();
adder.add(5);
adder.add(3);
console.log(adder.getTotal()); // 8 — Closure به total اصلی اشاره میکند
/* ─── اشتباه ۴: فراموش کردن Stale Closure در React ─── */
// در بخش React Hooks بالا توضیح داده شد
نکات مهم
Lexical Scope — جاوااسکریپت Scope را بر اساس جایی که کد نوشته شده تعیین میکند، نه جایی که اجرا میشود. این پایه درک Closure است.
Scope Chain — وقتی متغیری پیدا نشد، جاوااسکریپت به Scope بیرونی میرود — تا به Global Scope برسد. اگر آنجا هم نبود، ReferenceError میدهد.
هر تابع یک Closure است — در جاوااسکریپت همه توابع به محیط Lexical خود دسترسی دارند. Closure نه یک ویژگی خاص، بلکه رفتار طبیعی توابع است.
Closure = Reference نه Copy — Closure به متغیر اصلی اشاره میکند. اگر متغیر تغییر کند، Closure هم مقدار جدید را میبیند.
Memory Management — Closure هایی که دادههای بزرگ نگه میدارند میتوانند Memory Leak ایجاد کنند. فقط چیزی که نیاز دارید را در Closure نگه دارید.
var را فراموش کنید — همیشه
constو در صورت نیازletاستفاده کنید.varبا Function Scope رفتار غیرقابلپیشبینی دارد.
نتیجهگیری
Scope و Closure دو مفهومی هستند که وقتی واقعاً درک کنید، نوشتن کد جاوااسکریپت متحول میشود. دیگر رفتارهای عجیب کد شما را غافلگیر نمیکنند.
Scope تعیین میکند کجا متغیرها قابل دسترسی هستند. Closure تضمین میکند که توابع محیط اطرافشان را به خاطر میسپارند — حتی بعد از اتمام اجرای تابع بیرونی.
مسیر پیشنهادی برای تمرین:
روز ۱: انواع Scope را با مثالهای ساده تمرین کنید — متغیر بگذارید و ببینید کجا قابل دسترسی است
روز ۲: Counter و Module Pattern را پیادهسازی کنید
روز ۳: Debounce، Memoize و Rate Limiter را بسازید
روز ۴: یک State Manager ساده با Closure بسازید
Closure یکی از آن مفاهیمی است که اول گیجکننده به نظر میرسد، اما وقتی «کلیک» میکند، میفهمید چقدر قدرتمند است. تقریباً هر کتابخانه معروف جاوااسکریپت — از jQuery قدیمی تا React مدرن — به شکل گسترده از Closure استفاده میکند.
مقالات مرتبط
تابع در جاوااسکریپت — پایه درک Closure و Scope
متغیر در جاوااسکریپت — تفاوت var، let و const در Scope
Hoisting در جاوااسکریپت — رابطه Hoisting با Scope
Arrow Function در جاوااسکریپت — رفتار this و Closure در Arrow Function
ES6 در جاوااسکریپت چیست؟ — let، const و Block Scope

