Замыкания в JavaScript: как работают области видимости и почему глобальные переменные — это зло
Область видимости: Глобальные vs Локальные переменные
Область видимости (Scope) — это «территория» в коде, где переменная существует и доступна для использования. В JavaScript есть несколько уровней видимости, но для начала важно понять два основных:
- Глобальная область видимости: Переменные, объявленные вне функций или блоков, видны отовсюду. Они становятся свойствами глобального объекта (
windowв браузере,globalв Node.js). - Локальная область видимости: Переменные, объявленные внутри функции (с помощью
var,letилиconst), видны только внутри этой функции.
С появлением стандарта ES6 (ECMAScript 2015) появилась и блочная область видимости. Переменные, объявленные с помощью let и const внутри фигурных скобок {...} (например, в циклах или условных операторах), видны только внутри этого блока.
Проблема глобальных переменных: почему их стоит избегать
На первый взгляд, глобальные переменные кажутся удобными — они доступны везде. Но это их главный недостаток. Использование глобальных переменных считается плохой практикой по нескольким причинам:
- Конфликты имен: В большом проекте или при подключении сторонних скриптов легко случайно перезаписать существующую глобальную переменную, что приведет к непредсказуемым ошибкам.
- Уязвимость данных: Любая часть кода может изменить значение глобальной переменной. Это делает отладку настоящим кошмаром, так как источник изменения может находиться в совершенно другом файле.
- Загрязнение глобального пространства имен: Чем больше глобальных переменных, тем выше риск конфликтов и тем «грязнее» становится глобальный контекст.
- Проблемы с производительностью: Движку JavaScript требуется больше времени для поиска переменных в глобальной области видимости, так как он проверяет её в последнюю очередь после локальных областей.
Чтобы избежать этого, код стараются инкапсулировать. Раньше для этого использовали IIFE (Immediately Invoked Function Expression) — функцию, которая вызывается сразу после объявления:
javascript// Все переменные внутри IIFE - локальные и не видны снаружи
(function() {
var privateVar = "Я приватная";
console.log(privateVar);
})();
// console.log(privateVar); // Ошибка: privateVar is not defined В современном JavaScript для этого идеально подходят модули (import/export).
Что такое лексическое окружение (простыми словами)
Чтобы понять замыкания, нужно разобраться с понятием лексического окружения. Это внутренняя, скрытая структура, которую использует движок JavaScript для управления переменными.
У каждого блока кода, функции и самого скрипта есть свое лексическое окружение. Оно состоит из двух частей:
- Запись окружения (Environment Record): Место, где хранятся все переменные, объявленные в этой области.
- Ссылка на внешнее окружение (Outer Environment Reference): Указатель на лексическое окружение «снаружи».
Когда код обращается к переменной, движок сначала ищет её в текущем окружении. Если не находит — идет по ссылке во внешнее окружение, и так далее, пока не дойдет до глобального. Если переменная не найдена и там — возникает ошибка.
Замыкания: Магия сохранения состояния
Замыкание (Closure) — это функция, которая «запомнила» свое лексическое окружение. Технически, это способность функции получать доступ к переменным из внешней (лексической) области видимости даже после того, как эта внешняя функция уже завершила свое выполнение.
Как это работает под капотом (цепочка областей видимости)
Когда функция создается, она получает невидимое свойство [[Environment]], в котором хранится ссылка на то лексическое окружение, где она была создана. Когда функция вызывается, она создает свое новое окружение, но сохраняет ссылку на внешнее.
Представьте это как цепочку: Внутренняя_Функция -> Внешняя_Функция -> Глобальное_Окружение
Внутренняя функция всегда может «подняться» по этой цепочке и прочитать (а иногда и изменить) переменные из окружения внешней функции.
Практические примеры: от счетчика до мемоизации
Это не просто теория. Замыкания решают реальные задачи.
1. Фабрика функций и приватность (Счетчик) Это классический пример. С помощью замыкания мы создаем «приватную» переменную, к которой нет доступа извне.
Плохой подход (с глобальной переменной):
javascript// Глобальная переменная уязвима
let count = 0;
function badCounter() {
count++;
console.log("Счетчик:", count);
}
badCounter(); // 1
badCounter(); //