JavaScript: замыкания и контекст — глубокое погружение

Замыкания и контекст — темы, на которых спотыкаются даже опытные разработчики. Без их понимания вы будете писать баги в асинхронном коде, обработчиках событий и React-компонентах. Разбираем до мельчайших деталей.

JavaScript код

Лексическое окружение (Lexical Environment)

Всё начинается с лексического окружения. Это внутренний механизм JS, который хранит переменные и функции. Каждый блок кода, функция или скрипт имеют своё окружение.

// Глобальное окружение
let globalVar = 'Я виден везде';

function makeFunction() {
    // Локальное окружение функции
    let localVar = 'Я только внутри';
    console.log(globalVar); // ✅ Доступно
}
console.log(localVar); // ❌ ReferenceError

Когда код обращается к переменной, JS идёт по цепочке окружений: от самого вложенного до глобального. Это называется Scope Chain.

Что такое замыкание (Closure)?

Замыкание — это функция, которая «запоминает» своё лексическое окружение даже после того, как внешняя функция завершила выполнение.

function createCounter() {
    let count = 0;
    return function() {
        count++;
        return count;
    }
}

const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3

Переменная count должна была умереть после завершения createCounter(), но внутренняя функция «замкнула» её на себя. Это и есть замыкание.

💡 Зачем нужно замыкание? Инкапсуляция данных, создание приватных переменных, каррирование, мемоизация, обработчики событий в циклах.

Классическая ошибка с замыканием в цикле

// ❌ Ошибочный код
for (var i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i); // 3, 3, 3
    }, 100);
}

// ✅ Исправление через IIFE
for (var i = 0; i < 3; i++) {
    (function(j) {
        setTimeout(function() {
            console.log(j); // 0, 1, 2
        }, 100);
    })(i);
}

// ✅ Современное исправление (let)
for (let i = 0; i < 3; i++) {
    setTimeout(function() {
        console.log(i); // 0, 1, 2
    }, 100);
}

Почему так происходит? var не имеет блочной области видимости. Все три колбека ссылаются на одну переменную i, которая к моменту выполнения стала равна 3. let создаёт новую переменную для каждой итерации.

Контекст this: 4 правила определения

Значение this зависит от того, как вызвана функция. Запомните 4 простых правила.

1. Глобальный контекст

console.log(this); // window (браузер) или global (Node.js)

2. Вызов метода объекта

const user = {
    name: 'Алексей',
    greet() {
        console.log(this.name); // 'Алексей' — this ссылается на user
    }
};
user.greet();

3. Вызов функции (не метод)

function showThis() {
    console.log(this); // window (в strict mode — undefined)
}
showThis();

4. Вызов с new (конструктор)

function User(name) {
    this.name = name; // this ссылается на новый объект
}
const alex = new User('Алексей');

Явная привязка this: call, apply, bind

Иногда нужно принудительно задать this. Для этого есть три метода.

function greet(greeting, punctuation) {
    console.log(greeting + ', ' + this.name + punctuation);
}

const user = { name: 'Елена' };

// call — аргументы через запятую
greet.call(user, 'Привет', '!'); // Привет, Елена!

// apply — аргументы массивом
greet.apply(user, ['Здравствуйте', '...']);

// bind — создаёт новую функцию с привязанным this
const boundGreet = greet.bind(user, 'Йо');
boundGreet('!'); // Йо, Елена!
JavaScript код детали

Стрелочные функции и their this

Стрелочные функции не имеют своего this. Они берут его из внешнего лексического окружения.

const user = {
    name: 'Дмитрий',
    regularFunc: function() {
        console.log(this.name); // 'Дмитрий'
    },
    arrowFunc: () => {
        console.log(this.name); // undefined (this = window)
    }
};

Когда использовать стрелочные функции? В колбэках, чтобы не терять контекст. В методах объекта — осторожно, если нужен доступ к this объекта.

Практические примеры применения замыканий

1. Приватные переменные

function createBankAccount(initialBalance) {
    let balance = initialBalance;
    return {
        getBalance: () => balance,
        deposit: (amount) => { balance += amount; },
        withdraw: (amount) => {
            if (amount <= balance) balance -= amount;
        }
    };
}
const account = createBankAccount(1000);
console.log(account.getBalance()); // 1000
account.balance = 999999; // ❌ Не сработает, переменная приватная

2. Мемоизация (кэширование результатов)

function memoize(fn) {
    const cache = {};
    return function(...args) {
        const key = JSON.stringify(args);
        if (cache[key] === undefined) {
            cache[key] = fn(...args);
        }
        return cache[key];
    };
}

const slowSquare = (n) => {
    console.log('Вычисляю...');
    return n * n;
};
const fastSquare = memoize(slowSquare);
console.log(fastSquare(5)); // 'Вычисляю...' 25
console.log(fastSquare(5)); // 25 (из кэша, без вычисления)

3. Фабрика функций

function multiplyBy(factor) {
    return function(number) {
        return number * factor;
    };
}
const double = multiplyBy(2);
const triple = multiplyBy(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15
💡 Важно запомнить:
• Замыкание = функция + доступ к внешним переменным
• this определяется в момент вызова, а не создания
• Стрелочные функции не имеют своего this
• Для фиксации this используйте bind или стрелочные функции
• Замыкания потребляют память (переменные не удаляются сборщиком мусора, пока жива функция)

Заключение: Замыкания и контекст — фундаментальные концепции JS. Без них вы не поймёте, как работают React Hooks, асинхронный код, обработчики событий и многие паттерны. Практикуйтесь: напишите свой debounce, throttle, memoize. Изучите, как работают bind, call, apply. Через месяц эти концепции станут интуитивно понятными.