Территории памяти: Как работает область видимости и замыкания в JavaScript
Вы когда-нибудь задумывались, почему переменная, объявленная внутри функции, не видна снаружи? Или почему функция "запоминает" переменные из внешней функции даже после того, как та завершила работу?
Добро пожаловать в мир областей видимости и замыканий - фундаментальных концепций, которые делают JavaScript таким гибким и мощным. Это не просто теория. Это знание, которое отделяет новичка, случайно создающего глобальные переменные, от профессионала, пишущего чистый, предсказуемый и эффективный код.
Сегодня мы разберём, где живут переменные, как они умирают и как функция может "украсть" переменную из другой функции и удерживать её вечно.
Часть 1. Что такое область видимости (Scope)?
Область видимости - это территория, на которой переменная доступна. Это как правила дорожного движения для ваших переменных: где они видны, где их можно использовать, а где они исчезают.
javascript
// Глобальная область (видна всем)
const globalVar = "Я виден везде";
function myFunction() {
// Локальная область (видна только внутри функции)
const localVar = "Я виден только внутри myFunction";
console.log(globalVar); // ✅ Доступно
console.log(localVar); // ✅ Доступно
}
console.log(globalVar); // ✅ Доступно
console.log(localVar); // ❌ ReferenceError: localVar is not defined
Часть 2. Три короля областей видимости
2.1 Глобальная область (Global Scope)
Переменные, объявленные вне любой функции или блока, живут в глобальной области. Они доступны отовсюду.
javascript
// Глобальные переменные
let globalLet = "видна везде";
var globalVar = "тоже видна везде";
const GLOBAL_CONST = "константа";
function showGlobal() {
console.log(globalLet); // ✅
console.log(globalVar); // ✅
console.log(GLOBAL_CONST); // ✅
}
// В браузере глобальные переменные становятся свойствами window
console.log(window.globalVar);
⚠️ Опасность: Слишком много глобальных переменных - это загрязнение глобального пространства. Они могут конфликтовать, перезаписываться и приводить к трудноуловимым багам.
2.2 Функциональная область (Function Scope)
var имеет функциональную область видимости. Переменная видна во всей функции, независимо от того, где в функции она объявлена (благодаря hoisting).
javascript
function functionalScope() {
if (true) {
var insideIf = "я виден во всей функции";
}
console.log(insideIf); // ✅ "я виден во всей функции"
for (var i = 0; i < 3; i++) {
// i видна после цикла
}
console.log(i); // ✅ 3
}
// console.log(insideIf); // ❌ ReferenceError (вне функции)
2.3 Блочная область (Block Scope)
let и const имеют блочную область видимости. Блок - это всё, что находится между { }.
javascript
function blockScope() {
if (true) {
let insideIf = "я виден только внутри if";
const alsoInside = "тоже только внутри if";
}
// console.log(insideIf); // ❌ ReferenceError
for (let i = 0; i < 3; i++) {
// i существует только внутри цикла
}
// console.log(i); // ❌ ReferenceError
}
Часть 3. Вложенные области и цепочка областей видимости
Когда JavaScript ищет переменную, он идёт по цепочке: от самой внутренней области наружу, до глобальной.
javascript
const animal = "слон"; // глобальная
function outer() {
const animal = "тигр"; // область outer
function inner() {
const animal = "кот"; // область inner
console.log(animal); // "кот" (нашёл в своей области)
}
inner();
function inner2() {
console.log(animal); // "тигр" (не нашёл в inner2, поднялся в outer)
}
inner2();
}
outer();
console.log(animal); // "слон" (глобальная)
Правило: JavaScript ищет переменную в текущей области. Если не находит - поднимается на уровень выше. И так до глобальной области. Если и там нет - ошибка (в строгом режиме) или создание глобальной переменной (в нестрогом, что очень плохо).
Часть 4. Замыкание (Closure) - магия JavaScript
Замыкание - это способность функции "запоминать" переменные из внешней области видимости даже после того, как внешняя функция завершила выполнение.
4.1 Простейший пример замыкания
javascript
function createCounter() {
let count = 0; // переменная из внешней функции
return function() {
count++; // внутренняя функция "запоминает" count
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3
// Переменная count продолжает жить внутри counter!
// Она не видна снаружи
console.log(count); // ❌ ReferenceError
Что произошло? Функция, возвращённая из createCounter, создала замыкание. Она "захватила" переменную count и хранит её в своей области видимости. Даже после того, как createCounter завершилась, count осталась жива.
4.2 Как это работает под капотом?
Каждая функция в JavaScript при создании получает скрытое свойство [[Environment]], которое ссылается на лексическое окружение, где функция была создана. Когда функция вызывается, создаётся новое окружение, а внешним окружением для него становится то, что сохранено в [[Environment]].
javascript
// Псевдокод того, что происходит
function createCounter() {
// Создаётся окружение Env1 с переменной count
let count = 0;
// Функция запоминает Env1
return function() {
// При вызове ищет count в Env1
count++;
return count;
};
}
Часть 5. Классические примеры замыканий
5.1 Счётчик с шагом
javascript
function createStepCounter(step = 1) {
let count = 0;
return {
increment() {
count += step;
return count;
},
decrement() {
count -= step;
return count;
},
reset() {
count = 0;
return count;
},
getValue() {
return count;
}
};
}
const counter = createStepCounter(5);
console.log(counter.increment()); // 5
console.log(counter.increment()); // 10
console.log(counter.decrement()); // 5
console.log(counter.getValue()); // 5
5.2 Приватные переменные (до появления приватных полей)
javascript
function createBankAccount(initialBalance) {
let balance = initialBalance; // приватная переменная
return {
deposit(amount) {
if (amount > 0) {
balance += amount;
return balance;
}
throw new Error("Сумма должна быть положительной");
},
withdraw(amount) {
if (amount > 0 && amount <= balance) {
balance -= amount;
return balance;
}
throw new Error("Недостаточно средств");
},
getBalance() {
return balance;
}
};
}
const account = createBankAccount(1000);
account.deposit(500); // 1500
account.withdraw(200); // 1300
console.log(account.balance); // undefined (приватно!)
console.log(account.getBalance()); // 1300
5.3 Функции-фабрики
javascript
function createMultiplier(factor) {
return function(number) {
return number * factor;
};
}
const double = createMultiplier(2);
const triple = createMultiplier(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
5.4 Обработчики событий с сохранением состояния
javascript
function createButtonHandler(message) {
let clickCount = 0;
return function() {
clickCount++;
console.log(`${message} (нажато ${clickCount} раз)`);
};
}
const handleClick = createButtonHandler("Кнопка нажата");
// button.addEventListener("click", handleClick);
// Каждый клик будет увеличивать счётчик
Часть 6. Классическая ловушка: замыкание в циклах
Один из самых известных подводных камней JavaScript.
javascript
// Проблема с var
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 3, 3, 3 (все три!)
}, 100);
}
// Почему? Все три функции замыкаются на одну и ту же переменную i.
// К моменту вызова setTimeout (через 100мс) цикл уже завершился, i = 3.
Решения:
Решение 1: Использовать let (создаёт новую переменную для каждой итерации)
javascript
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i); // 0, 1, 2
}, 100);
}
Решение 2: IIFE (создаёт новую область видимости для каждой итерации)
javascript
for (var i = 0; i < 3; i++) {
(function(j) {
setTimeout(function() {
console.log(j); // 0, 1, 2
}, 100);
})(i);
}
Решение 3: Функция-фабрика
javascript
function createLogger(value) {
return function() {
console.log(value);
};
}
for (var i = 0; i < 3; i++) {
setTimeout(createLogger(i), 100);
}
Часть 7. Замыкания и память: осторожно, утечки!
Замыкания удерживают переменные в памяти. Если неосторожно использовать их, можно создать утечку памяти.
javascript
function createHeavyClosure() {
const hugeArray = new Array(1000000).fill("данные"); // занимает много памяти
return function() {
// Всего лишь возвращает число, но hugeArray всё ещё в замыкании
return 42;
};
}
const closure = createHeavyClosure();
// hugeArray никогда не будет удалён, даже если closure использует только 42!
Решение: Используйте только нужные переменные.
javascript
function createLightClosure() {
const hugeArray = new Array(1000000).fill("данные");
const result = 42; // берём только то, что нужно
return function() {
return result; // hugeArray может быть собран сборщиком мусора
};
}
Часть 8. Модульный паттерн (через замыкания)
До появления ES6 модулей замыкания использовались для создания модулей.
javascript
const calculator = (function() {
// Приватные переменные и методы
let result = 0;
function validateNumber(n) {
if (typeof n !== "number") throw new Error("Нужно число");
return n;
}
// Публичное API
return {
add(n) {
result += validateNumber(n);
return this;
},
subtract(n) {
result -= validateNumber(n);
return this;
},
multiply(n) {
result *= validateNumber(n);
return this;
},
getResult() {
return result;
},
reset() {
result = 0;
return this;
}
};
})();
calculator.add(5).multiply(2).subtract(3);
console.log(calculator.getResult()); // 7
console.log(calculator.result); // undefined (приватно)
Часть 9. var, let, const: битва за область видимости
javascript
// var - функциональная область
if (true) {
var x = 10;
}
console.log(x); // 10 (вылезло из блока!)
// let/const - блочная область
if (true) {
let y = 20;
const z = 30;
}
// console.log(y); // Ошибка!
// console.log(z); // Ошибка!
// Temporal Dead Zone (TDZ)
// console.log(tdz); // ReferenceError (TDZ)
let tdz = "теперь можно";
Часть 10. Реальные кейсы из продакшена
10.1 Дебаунс (Debounce) - ограничение частоты вызовов
javascript
function debounce(fn, delay) {
let timeoutId = null; // переменная живёт в замыкании
return function(...args) {
// Очищаем предыдущий таймер
if (timeoutId) clearTimeout(timeoutId);
// Устанавливаем новый
timeoutId = setTimeout(() => {
fn.apply(this, args);
timeoutId = null;
}, delay);
};
}
const debouncedSearch = debounce((query) => {
console.log(`Поиск: ${query}`);
}, 300);
// При быстром вводе вызовется только последний
debouncedSearch("a");
debouncedSearch("ap");
debouncedSearch("app");
debouncedSearch("appl");
debouncedSearch("apple"); // только этот выполнится через 300мс
10.2 Троттлинг (Throttle) - не чаще раза в интервал
javascript
function throttle(fn, limit) {
let inThrottle = false;
let lastArgs = null;
let lastThis = null;
function execute() {
if (lastArgs) {
fn.apply(lastThis, lastArgs);
lastArgs = null;
lastThis = null;
setTimeout(execute, limit);
} else {
inThrottle = false;
}
}
return function(...args) {
if (!inThrottle) {
fn.apply(this, args);
inThrottle = true;
setTimeout(execute, limit);
} else {
lastArgs = args;
lastThis = this;
}
};
}
const throttledScroll = throttle(() => {
console.log("Скролл!", Date.now());
}, 1000);
window.addEventListener("scroll", throttledScroll);
10.3 Один раз (once)
javascript
function once(fn) {
let executed = false;
let result;
return function(...args) {
if (!executed) {
executed = true;
result = fn.apply(this, args);
}
return result;
};
}
const initialize = once(() => {
console.log("Инициализация...");
return { status: "ready" };
});
console.log(initialize()); // "Инициализация..." → { status: "ready" }
console.log(initialize()); // { status: "ready" } (без повторной инициализации)
Часть 11. Подводные камни
Камень #1: Потеря this в замыкании
javascript
const obj = {
name: "Анна",
createLogger() {
return function() {
console.log(this.name); // this = global (или undefined)
};
}
};
const logger = obj.createLogger();
logger(); // undefined
// Решение: стрелочная функция или bind
const obj2 = {
name: "Анна",
createLogger() {
return () => {
console.log(this.name); // this = obj2
};
}
};
Камень #2: Создание замыкания в цикле с асинхронностью (уже разобрали)
Камень #3: Неожиданное поведение с var
javascript
var funcs = [];
for (var i = 0; i < 3; i++) {
funcs.push(() => console.log(i));
}
funcs[0](); // 3
funcs[1](); // 3
funcs[2](); // 3
Итог: Манифест областей видимости и замыканий
- Глобальная область - доступна везде. Не злоупотребляйте.
- Функциональная область - у var. Переменная видна во всей функции.
- Блочная область - у let и const. Переменная видна только в блоке { }.
- Цепочка областей - JS ищет переменную от внутренней области к внешней.
- Замыкание - функция "запоминает" переменные из внешней области, даже после её завершения.
- IIFE - старый способ создания замыканий (сейчас используется реже).
- Утечки памяти - замыкания удерживают переменные, будьте осторожны.
- let в циклах - решает проблему с асинхронностью (создаёт новую переменную для каждой итерации).
Финальный тест (что выведет?):
javascript
let x = 10;
function outer() {
let x = 20;
function inner() {
console.log(x);
}
return inner;
}
const fn = outer();
fn();
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
Ответы:
- 20 (замыкание захватило x из outer, а не глобальный)
- 3, 3, 3 (var - одна переменная на весь цикл)
- 0, 1, 2 (let - новая переменная для каждой итерации)
Области видимости и замыкания - это фундамент JavaScript. Понимание того, где живут переменные и как функции захватывают окружение, позволяет писать предсказуемый, эффективный и красивый код. Это знание превращает магию в инженерию, а случайности - в осознанные решения. Используйте его мудро.