Замыкание (англ. closure) – функция, в теле которой присутствуют ссылки на переменные, объявленные вне тела этой функции в окружающем коде и не являющиеся её локальными переменными или передаваемыми параметрами. Говоря другим языком, замыкание – функция, которая ссылается на свободные переменные в своей области видимости.

Как и в большинстве современных языков программирования, в JavaScript используются лексические области видимости, или лексический контекст (англ. lexical scope).

Если одна функция определена внутри другой, внутренняя имеет доступ к области видимости внешней. Это называется «лексической областью видимости» или «замыканием».

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

Простой для понимания пример:

Выполнить код »

Интерпретатор JavaScript на стадии инициализации, ещё до выполнения первой строчки кода, создает пустой объект LexicalEnvironment и заполняет его:

  • В нашем случае есть локальная переменная name, у которой сразу есть значение (аргумент, который мы передаем) и это "John".
  • Как известно, интерпретатор JavaScript «поднимает» объявления переменных в начало области видимости, при этом, присваивание значений переменным не поднимается вместе с их объявлением. Именно по этому у нас в самом начале функции уже есть переменная phrase и она равна undefined.

На стадии выполнения функции происходит присвоение локальной переменной phrase, и наш объект LexicalEnvironment меняется. Его свойство phrase становится равным тому, что мы записали – "Hello, John".

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

А теперь давайте как будет вести себя функция, если в её теле не объявлена локальная переменная:

Выполнить код »

В JavaScript есть скрытое свойство, которое называется [[Scope]]. Когда функция объявляется, то она всегда объявляется в какой-либо области. Эта функция может быть объявлена в другой функции, в глобальном объекте и т.д. В нашем случае функция объявлена в глобальном объекте window, поэтому свойство greeting.[[Scope]] равно window.

Интерпретатор JavaScript, при доступе к переменной, сначала пытается найти переменную в текущем объекте LexicalEnvironment, а затем, если её нет – ищет во внешнем объекте переменных. В нашем случае переменную name интерпретатор возьмет из объекта LexicalEnvironment, а переменную phrase из объекта window. Конечно, если у нас будет одноименная локальная переменная phrase, то она запишется в объект LexicalEnvironment и впоследствии будет взята оттуда, а не из внешней области видимости.

Обратите внимание, что свойство [[Scope]] устанавливается по тому месту, где функция была объявлена, а не вызвана, именно поэтому код ниже выведет Hello, John, а не Bye, John:

Выполнить код »

Как было сказано выше, в конце выполнения функции объект с переменными LexicalEnvironment обычно уничтожается и память очищается. В примерах выше так и происходит.
Однако что, если мы хотим сохранить эти данные? Т.е. мы хотим, чтобы все, что записано в объект LexicalEnvironment сейчас, сохранилось и было доступно при следующих вызовах? Именно для этого и существуют замыкания.

Итак, замыкание – это область действия, которая создается при объявлении функции и позволяет ей получать доступ к внешними (свободным) по отношению к ней переменными.

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

Это понятие лучше всего пояснить на примере конкретного кода. Поэтому начнем с небольшого кода, реализующего функцию-счетчик:

Выполнить код »

В приведенном выше примере кода переменная count и функция counter объявляются в одной и той же области действия (в данном случае в глобальной window). После этого функция выполняется, при этом переменная count доступна для данной функции. Мы с вами только что невольно создали самое простое замыкание!

Приведенный пример, по-видимому, вас не особенно впечатляет. Здесь переменная и функция объявлены в глобальной области действия, которая вообще не завершается (пока страница загружена), и поэтому переменная count существует в данной области действия и по-прежнему доступна для функции.

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

Выполнить код »

В этом примере мы вызываем функцию outerCounter, которая создает функцию counter и возвращает ее вместе с окружением, содержащим свободную переменную count. Обратите внимание, что мы возвращаем функцию, а не её значение (return counter, а не return counter()). Таким образом создаётся замыкание. Функция, возвращенная из outerCounter, сохраняется в runCount.

Для глобальной области действия переменная count остается невидимой. Тем не менее мы можем использовать ее при каждом вызове функции runCount. К переменной count невозможно обратиться иначе, кроме как при вызове runCount.

Далее в примере мы outerCounter присваиваем null, т.е. мы просто уничтожаем нашу функцию outerCounter. Однако, когда мы вызовем runCount(), то увидим очередное значение переменной count функции outerCounter. Как такое возможно? Все дело в том, что наша возвращаемая функция counter() также имеет свойство [[Scope]], которое ссылается на внешнюю область видимости, а эта внешняя область видимости в нашем случае – объект LexicalEnvironment нашей функции outerCounter. Объект LexicalEnvironment не удалился вместе с функцией outerCounter и остался в памяти, и будет оставаться в памяти до тех пор, пока на него будет хотя бы одна ссылка. В нашем случае эта ссылка – возвращаемая функция counter, которая использует переменную count этого объекта LexicalEnvironment.

Примечание: В рассмотренном примере используется именованное функциональное выражение. Это не обязательно, но удобно для ссылок на имя внутренней функции. Возвращаемая функция должна вызываться по ссылке на внешнюю функцию runCount (а не counter).

Упростим немножко пример выше – уберём необходимость отдельно вызывать функцию outerCounter, сделав ее аномимной и вызвав сразу же после ее объявления:

Выполнить код »

Следующий пример демонстрирует создание двух замыканий вокруг одной функции:

Выполнить код »

Здесь, как и в предыдущем примере, вызов внешней функции outer() возвращает другую внутреннюю функцию. Внутренняя функция inner запоминает окружение, в котором она была создана, в частности, значение переменной c.

Переменная fun1 представляет собой замыкание: функцию inner и окружение, в котором функция была создана. Окружение в нашем случае состоит из локальной переменной c (свободной по отношению к функции inner), которая была в области действия функции outer() во время создания замыкания.

В нашем случае создаётся два замыкания: fun1 и fun2, для каждого из которых, в свою очередь, создается свое окружение.

Передача аргументов для внутренней и внешней функций при определении замыкания:

var fun1 = outer(3);           

Аргумент 3 передается для параметра a внешней функции outer().

При вызове внутренней функции inner:

var result1 = fun1(7);          

Аргумент 7 передается для параметра b во внутреннюю функцию inner.

Есть и другой способ вызова замыкания:

Выполнить код »
Рассмотрим еще один способ создания замыканий. Мы создадим замыкание в обработчике события — кстати, этот способ довольно часто встречается в коде JavaScript. Для начала мы создадим простую веб-страницу с кнопкой и элементом
для вывода сообщения. Программа будет отслеживать количество нажатий кнопки, а результат будет отображаться в
.
Выполнить код »

Браузер создает замыкание для функции, назначаемой свойству button.onclick. В окружении задействованы свободные переменные div, message и count:

function() {
        count++;
        div.innerHTML = message + count;
};
var message = "Количество кликов: ";
var count = 0;
var div = [object]

В этом примере замыкание продолжает существовать до закрытия страницы.

  • Если одна функция определена внутри другой, внутренняя имеет доступ к области видимости внешней – это называется «лексической областью видимости», или «замыканием».
  • Замыкание — это функция, ссылающаяся на независимые (свободные) переменные. Иными словами, замыкание состоит из функции и используемого ею окружения.
  • В замыкании отражены значения переменных, находившихся в области действия на момент создания замыкания.
  • Чтобы получить значение переменной во вложенной функции, используйте значение, определяемое в ближайшей внешней функции и т.д. Если найти значение не удалось, обращайтесь к глобальной области действия.
  • Каждый запуск функции создает специальный объект LexicalEnvironment свойствами, которого являются все переменные и параметры этой функции. На верхнем уровне располагается «глобальный объект», в браузере это window.
  • При создании функция получает системное свойство [[Scope]], которое ссылается на специальный объект LexicalEnvironment, в котором она была создана. Системное свойство [[Scope]] устанавливается по тому месту, где функция была объявлена, а не вызвана

  • Ввести пароль

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

    function makePassword(password) {
        return /*      ваш код    */ {
            return (tryPassword === password);
        };
    }
        /*      ваш код     */
    
    Показать решение

    Решение:

    1. Функция (назовём её nick), возвращаемая из внешней функции makePassword, представляет собой замыкание с окружением, содержащим свободную переменную password.
    2. Мы передаем функции makePassword значение rightly, которое сохраняется в окружении замыкания: var tryNick = makePassword("rightly")
    3. При вызове tryNick переданное слово ("wrong" или "rightly") сравнивается со значением password в окружении tryNick.
    Выполнить код »
  • Сложение n + m

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

    function addition(n) {
        return /*    ваш код    */ {
            return /*    ваш код    */;
        };
    }
        /*      ваш код     */
    
    Показать решение

    Решение:

    1. Функция addTo, возвращаемая из addition, представляет собой замыкание с окружением, содержащим свободную переменную n.
    2. Мы передаем функции addition значение 3, которое сохраняется в окружении замыкания: var res = addition(3);
    3. Вызов addition(3) возвращает функцию addTo, которая прибавляет любое переданное число к 3.
    Выполнить код »
  • Одноимённые переменные

    Каков будет результат выполнения приведенного кода и почему?

    var num = 7;
    
    function getNum() {
        var num = 8;
        function getValue() {
            return num;
        }
        return getValue;
    }
    var res = getNum();
    alert( res() );
    
    Показать решение

    Решение:

    num – свободная переменная в функции getValue, поэтому она сохраняется в окружении getValue.

    Мы возвращаем getValue из getNum, поэтому здесь создается замыкание, возвращаемое из getNum. Соответственно, при вызове res (getValue) в другом контексте (глобальной области действия) используется значение secret из окружения.

    Ответ: 8

    Выполнить код »
  • Две ссылки на одну функцию

    Какой результат будет выведен при очередном вызове generation и generation2? Почему?

    function numberGenerator() {
      var currentNum = 2;
      return function multNumber() {      
        return currentNum *= currentNum;
      };
    }
    
    var generation = numberGenerator();   
    var generation2 = numberGenerator();  
    
    alert( generation() );             
    alert( generation() );             
    
    alert( generation2() );            
    alert( generation2() );             
    
    Показать решение

    Решение:

    1. В процессе выполнения внешняя функция numberGenerator создаёт возвращаемую функцию multNumber. При создании функция multNumber получает внутреннее свойство [[Scope]] со ссылкой на текущий объект LexicalEnvironment.
    2. Когда вызов функции numberGenerator завершается – возвращается функция multNumber и сохраняется во внешней переменной generation.
    3. Возвращённая из numberGenerator функция generation, благодаря полученному свойству [[Scope]], помнит в каком окружении была создана.
    4. Таким образом generation и generation2 независимы друг от друга, потому что при каждом запуске numberGenerator создаётся свой объект переменных LexicalEnvironment со своим текущим значением currentNum.
    Выполнить код »

Комментарии

пожелания к комментариям…
  • Приветствуются комментарии, соответствующие теме урока: вопросы, ответы, предложения.
  • Одну строчку кода оборачивайте в тег <code>, несколько строчек кода — в теги <pre><code>...ваш код...</code></pre>.
  • Допускаются ссылки на онлайн-песочницы (codepen, plnkr, JSBin и др.).