Avatar ·

Как работают замыкания (Closures) в JavaScript? Объясните на простом примере

Всем привет! Я только начал изучать JavaScript и никак не могу понять тему замыканий (closures). Читал разные статьи, но всё равно путаюсь. Вот например, есть такой код:
function counter() {
    let count = 0;
    return function() {
        count++;
        return count;
    };
}

const myCounter = counter();
console.log(myCounter()); // 1
console.log(myCounter()); // 2
console.log(myCounter()); // 3
Почему переменная count не сбрасывается каждый раз? Ведь функция counter() уже завершила свою работу, а мы всё ещё можем менять её внутреннюю переменную. Я ожидал, что после первого вызова myCounter() переменная count будет уничтожена, так как она объявлена внутри функции. И ещё вопрос: зачем вообще нужны замыкания? В каких реальных ситуациях они применяются? Буду благодарен за простое объяснение с примерами, желательно на русском. Заранее спасибо! <<>>

Что такое замыкание и как оно работает?

Замыкание (closure) — это комбинация функции и её лексического окружения (scope), в котором эта функция была объявлена. Простыми словами: функция "запоминает" переменные из внешней функции, даже после того, как внешняя функция завершила выполнение.

Разберём ваш пример по шагам:

function counter() {
    let count = 0;  // (1) создаётся переменная count
    return function() {  // (2) возвращаем внутреннюю функцию
        count++;         // (3) увеличиваем count
        return count;    // (4) возвращаем новое значение
    };
}

const myCounter = counter(); // (5) вызываем counter, получаем внутреннюю функцию
console.log(myCounter()); // 1  // (6) вызываем внутреннюю функцию
console.log(myCounter()); // 2
console.log(myCounter()); // 3

Как это работает под капотом:

  • Когда мы вызываем counter(), JavaScript создаёт локальную переменную count со значением 0.
  • Внутренняя функция (анонимная функция) создаётся внутри этого окружения. Она "захватывает" ссылку на переменную count.
  • Когда counter() завершает работу, её локальное окружение обычно должно быть уничтожено. НО! Так как внутренняя функция всё ещё существует (мы сохранили её в переменную myCounter) и ссылается на count, JavaScript сохраняет это окружение в памяти.
  • Таким образом, myCounter — это не просто функция, а замыкание, которое содержит внутри себя и код функции, и "законсервированную" переменную count.

Каждый раз, когда мы вызываем myCounter(), мы обращаемся к той самой переменной count, которая "живёт" внутри замыкания. Поэтому она не сбрасывается, а увеличивается.

Реальные примеры использования замыканий

1. Создание приватных переменных (инкапсуляция)

function createUser(name) {
    let password = 'secret123';  // приватная переменная
    return {
        getName: function() { return name; },
        checkPassword: function(pass) { return pass === password; }
    };
}

const user = createUser('Иван');
console.log(user.getName()); // 'Иван'
console.log(user.password); // undefined (недоступно снаружи)
console.log(user.checkPassword('secret123')); // true

2. Создание функций с "памятью" (мемоизация)

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

3. Обработчики событий и асинхронность

function createButtons() {
    for (var i = 1; i <= 3; i++) {
        (function(index) {  // замыкание сохраняет текущее значение i
            setTimeout(function() {
                console.log('Кнопка ' + index);
            }, index * 1000);
        })(i);
    }
}
createButtons(); // "Кнопка 1", "Кнопка 2", "Кнопка 3" с интервалом

Важно: Если бы мы не использовали замыкание, все setTimeout вывели бы "Кнопка 4", потому что переменная i к моменту выполнения колбэков уже стала бы равна 4.

Замыкания — одна из самых мощных и фундаментальных концепций JavaScript. Они позволяют создавать чистый, модульный и безопасный код. Понимание замыканий откроет вам путь к продвинутым паттернам программирования.

<<>>

Простое объяснение через "рюкзак"

Представьте, что каждая функция — это человек, а замыкание — это рюкзак, который человек носит с собой. Когда функция создаётся, она кладёт в свой рюкзак все переменные, которые видит вокруг себя. Даже если потом внешняя функция "уйдёт", рюкзак остаётся.

В вашем примере: counter() уходит, но её внутренняя функция уносит с собой рюкзак с переменной count. Каждый раз, когда вы вызываете myCounter(), она достаёт из рюкзака count, увеличивает его и кладёт обратно.

Альтернативный подход: использование методов объекта

Замыкания можно заменить методами объекта, но это менее гибко:

const counterObj = {
    count: 0,
    increment: function() {
        this.count++;
        return this.count;
    }
};

console.log(counterObj.increment()); // 1
console.log(counterObj.increment()); // 2

Проблема этого подхода: переменная count публичная, её можно случайно изменить: counterObj.count = 100. Замыкание же даёт настоящую приватность.

Совет новичкам

Чтобы лучше понять замыкания, попробуйте написать код, в котором одна функция возвращает другую, и поэкспериментируйте в консоли браузера или на JSFiddle. Поставьте debugger внутри и посмотрите, какие переменные доступны на каждом шаге. Практика — лучший способ освоить эту тему.