JavaScript. Замыкание области видимости

12.06.2021

Теги: JavaScriptWeb-разработкаТеорияФункция

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

function foo() {
    var a = 2;
    function bar() {
        console.log(a); // 2
    }
    bar();
}

foo();

Функция bar() обладает доступом к переменной a во внешней области видимости из-за правил поиска лексической области видимости. Другими словами, функция bar() обладает замыканием над областью видимости foo() (и над над глобальной областью видимости). Почему? Потому что функция bar() вложена в foo().

Однако замыкания, определяемые таким образом, не видны напрямую. Давайте рассмотрим код, который выводит замыкание на свет.

function foo() {
    var a = 2;
    function bar() {
        console.log(a);
    }
    return bar;
}

var baz = foo();
baz(); // 2

В данном случае функция bar() выполняется за пределами лексической области видимости. После выполнения foo() можно ождать, что вся внутренняя область видимости foo() исчезает, потому что больше не используется. Но «волшебство» замыканий не позволяет этому случиться. Внутренняя область видимости на самом деле продолжает использоваться, и поэтому не пропадает. Функция bar() все еще содержит ссылку на эту область видимости, и эта ссылка называется замыканием.

В примере ниже функция baz() тоже вызывается за пределами своей лексической области видимости. И она обладает замыканием над внутренней областью видимости функции foo(), благодаря чему получает доступ к переменной a.

function foo() {
    var a = 2;
    function baz() {
        console.log(a); // 2
    }
    bar(baz);
}
function bar(fn) {
    fn(); // смотрите, замыкание!
}

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

function wait(message) {
    setTimeout(function timer() {
        console.log(message);
    }, 1000);
}

wait('Привет, замыкание!');

Мы берем внутреннюю функцию (с именем timer) и передаем ее setTimeout(). Однако функция timer имеет замыкание над областью видимости wait(), вследствие чего эта функция поддерживает и использует ссылку на переменную message.

Циклы и замыкания

Самый частый и канонический пример, используемый для демонстрации замыканий, основан на обычном цикле for.

for (var i = 1; i <= 5; i++) {
    setTimeout(function timer() {
        console.log(i);
    }, i * 1000);
}

По духу этого фрагмента кода можно было бы ожидать, что он выведет числа 1, 2, …, 5 — по одному каждую секунду. На самом деле при выполнении этого кода число 6 будет выведено пять раз с односекундными интервалами.

Сначала разберемся, откуда берется число 6. Цикл продолжается, пока выполняется условие i<=5. В первой итерации, в которой оно не будет выполняться, значение i будет равно 6. Итак, в выводе отражено итоговое значение i после завершения цикла.

Мы пытались сделать так, чтобы каждая итерация цикла «захватывала» собственную копию i на момент итерации. Но получилось так, что все пять функций, хотя они и определяются отдельно при каждой итерации цикла, замыкаются над одной общей глобальной областью видимости, которая на самом деле содержит только один экземпляр i.

Чего не хватает? Нужна более замкнутая область видимости. А именно — для каждой итерации цикла должна создаваться новая замкнутая область видимости. Давайте сделаем это.

for (var i = 1; i <= 5; i++) {
    (function () {
        setTimeout(function timer() {
            console.log(i);
        }, i * 1000);
    })();
}

Не работает. Но почему? Каждая функция обратного вызова тайм-аута замыкается над своей областью видимости уровня итерации. Но эта область видимости пуста, потому что не содержит переменных. И при выполнении console.log(i) переменная i будет получена из глобальной области видимости. Еще одна попытка.

for (var i = 1; i <= 5; i++) {
    (function () {
        var j = i;
        setTimeout(function timer() {
            console.log(j);
        }, j * 1000);
    })();
}

Теперь все работает правильно. Немедленный вызов анонимной функции создает замкнутую область видимости, функция timer имеет к ней доступ. А главное — в этой области видимости есть переменная j, которая сохраняет текущее значение переменной i.

Вместо того, чтобы создавать замкнутую область видимости с помощью немедленного вызова анонимной функции, можно использовать блочную область видимости тела цикла — если для объявления переменной использовать ключевое слово let.

for (var i = 1; i <= 5; i++) {
    let j = i; // блочная область видимости для замыкания!
    setTimeout(function timer() {
        console.log(j);
    }, j * 1000);
}

Но и это еще не все! Для объявлений let в заголовке цикла for определено специальное поведение. Оно означает, что переменная объявляется не однократно для цикла, а для каждой итерации. И она (для нашего удобства) при каждой последующей итерации будет инициализироваться значением на конец предыдущей итерации.

for (let i = 1; i <= 5; i++) {
    setTimeout(function timer() {
        console.log(i);
    }, i * 1000);
}

Поиск: JavaScript • Web-разработка • Теория • Функция • Замыкание

Каталог оборудования
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Производители
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Функциональные группы
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.