Замыкания в JavaScript

Давайте теперь изучим понятие замыкание (англ. closure). На самом деле вы уже знакомы с этим понятием, осталось только узнать правильную терминологию.

Итак, замыкание - это функция вместе со всеми внешними переменными, которые ей доступны. Или, другими словами, замыкание - это функция вместе со своим лексическим окружением.

В JavaScript чаще всего, говоря "замыкание функции", имеют ввиду не саму эту функцию, а именно ее внешние переменные. Если же какая-то функция получает переменную из своего лексического окружения, то говорят "переменная берется из замыкания".

Вспомним код, который мы сделали в предыдущем уроке:

function test() { let num = 1; return function() { alert(num); } } let func = test(); func(); // выведет 1

В данном случае и можно сказать, что функция func получает значение переменной num из замыкания. Также можно сказать, что функция func хранит значение переменной num в замыкании.

Счетчик на замыканиях

Давайте перепишем рассмотренный нами код так, чтобы возвращаемая функция каждый раз увеличивала значение переменной num на единицу:

function test() { let num = 1; return function() { alert(num); num++; // прибавляем единицу } } let func = test();

Получится, что каждый вызов функции func будет выводить на экран новое значение:

function test() { let num = 1; return function() { alert(num); num++; } } let func = test(); func(); //выведет 1 func(); //выведет 2 func(); //выведет 3 func(); //выведет 4 func(); //выведет 5

Получается, что мы реализовали счетчик вызова функций, используя замыкание (точнее используя переменную num из замыкания нашей функции).

Учтите, что каждый вызов функции test будет возвращать новую функцию, у которой будет свое замыкание. То есть разные счетчики будут работать независимо:

function test() { let num = 1; return function() { alert(num); num++; }; }; let func1 = test(); // первый счетчик func1(); //выведет 1 func1(); //выведет 2 let func2 = test(); // второй счетчик func2(); //выведет 1 func2(); //выведет 2

Получается, что одна и та же переменная num для разных функций будет иметь разное значение!

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

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

Пусть функция в замыкании хранит число 10. Сделайте так, чтобы каждый вызов функции уменьшал это число на 1 и выводил на экран уменьшенное число.

Модифицируйте предыдущую задачу так, чтобы отсчет доходил до 0, а затем каждый последующий вызов функции выводил на экран сообщение о том, что отсчет окончен.

Нюанс

Рассмотрим следующий код:

function test() { let num = 1; return function() { alert(num); num++; }; }; test()(); //выведет 1 test()(); //выведет 1

Почему всегда будет выводится число 1? Для того, чтобы понять это, перепишем наш код по другому:

function test() { let num = 1; return function() { alert(num); num++; }; }; let func1 = test(); //!! первая функция func1(); //выведет 1 let func2 = test(); //!! вторая функция func2(); //выведет 1

То есть каждый вызов функции test таким образом: test()(), создает свою функцию со своим замыканием и сразу же вызывает эту функцию.

Определите, не запуская код, что выведется на экран:

function func() { let num = 0; return function() { alert(num); num++; }; }; func()(); func()(); func()();

Определите, не запуская код, что выведется на экран:

function func() { let num = 0; return function() { alert(num); num++; }; }; let test = func; test()(); test()(); test()();

Нюанс

Вынесем переменную num за функции, тем самым сделав ее глобальной:

let num = 1; // глобальная переменная function test() { return function() { alert(num); num++; }; };

В этом случае все возвращаемые функции будут изменять эту глобальную переменную и счетчики будут работать уже зависимо друг от друга:

let num = 1; function test() { return function() { alert(num); num++; }; }; let func1 = test(); // первый счетчик func1(); //выведет 1 func1(); //выведет 2 let func2 = test(); // второй счетчик func2(); //выведет 3 func2(); //выведет 4

Почему же наш предыдущий код делал независимые счетчики? Напомню этот код:

function test() { let num = 1; return function() { alert(num); num++; }; };

Дело в том, что переменная num - локальная внутри функции test. Поэтому каждый вызов test порождает свою локальную переменную.

Поэтому возвращаемые функции будут ссылаться каждая на свою локальную переменную функции test. Именно так и достигается независимость работы.

Если же сделать num глобальной переменной - это тоже будет замыканием. Просто лексические окружения возвращаемых функций ссылаются на одну и ту же переменную num - любые изменения с этой переменной будут видны во всех функциях.

Определите, не запуская код, что выведется на экран:

let counter = 0; function test() { return function() { alert(counter); counter++; }; }; let func = test; let func1 = func(); let func2 = func(); func1(); func2(); func1(); func2();

Определите, не запуская код, что выведется на экран:

function test() { let counter = 0; return function() { return function() { alert(counter); counter++; }; }; }; let func = test()(); let func1 = func; let func2 = func; func1(); func2(); func1(); func2();

Определите, не запуская код, что выведется на экран:

function test() { let counter = 0; return function() { return function() { alert(counter); counter++; }; }; }; let func = test(); let func1 = func(); let func2 = func(); func1(); func2(); func1(); func2();