Замыкания, вложенные функции

На этом занятии подробно разберем области видимости данных в JavaScript и такое важное понятие как замыкание.

Для начала представим, что у нас имеется вот такая программа:

let name = "Иван";
getName("Привет!");
 
function getName(say) {
    console.log(name + ": " + say);
}

Как это работает в деталях. Как мы уже говорили, все функции, объявленные как Function Declaration, создаются JavaScript-движком в первую очередь. Но что значит создаются? В действительности, при запуске любого скрипта создается специальный объект (на уровне JavaScript-машины и недоступный программисту), называемый глобальным лексическим окружением (Lexical Envirnoment). Он состоит из двух частей:

  1. Environment Record – объект, в котором хранятся локальные данные как свойства этого объекта;
  2. Ссылка на внешнее лексическое окружение (если его нет, то эта ссылка равна null).

Так вот, объявление функции getName – это не что иное, как создание свойства getName в глобальном лексическом окружении:

Далее, начинает выполняться скрипт. Создается переменная name. Значит, в лексическом окружении появляется еще одно свойство – name:

Затем идет вызов функции getName("Привет!"). В лексическом окружении ищется свойство getName со ссылкой на функцию. Так как оно там уже есть, то функция успешно вызывается. Но, каждый вызов функции создает свое лексическое окружение и картина будет выглядеть так:

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

console.log(name + ": " + say);

Переменная name сначала ищется в лексическом окружении функции. Не находит ее, тогда процесс поиска повторяется на следующем уровне, на который ссылается outer. В глобальном окружении переменная name существует, берется ее значение для вывода в консоль. То же самое происходит и с переменной say. Но она имеется в лексическом окружении самой функции, поэтому поиск здесь и завершается, не переходя к глобальному пространству. В результате мы видим сообщение «Иван: Привет!».

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

Из этого примера следуют такие важные моменты:

  1. Внешнее лексическое окружение не имеет доступа к данным внутренних окружений.
  2. Поиск переменных начинается с текущего окружения и при необходимости последовательно переходит к внешним окружениям. Останавливается, как только переменная найдена.
  3. Лексическое окружение автоматически уничтожается, когда в нем более нет необходимости (на него нет внешних ссылок).

Здесь нужно лишь добавить: если переменная не находится, то при включенном режиме "use strict" она принимает значение undefined. Иначе, будет ссылаться на глобальный объект (в браузере – это window).

Исходя из всего сказанного, легко понять, что выведет в консоль следующий скрипт:

let name = "Иван";
getName("Привет!");
 
function getName(say) {
    let name = "Федор";
    console.log(name + ": " + say);
}

Да, мы видим строчку: «Федор: Привет!». То есть, переменная name была найдена в локальном окружении и взято значение «Федор», а не «Иван».

А вот, если мы выведем что-то в глобальном лексическом окружении:

console.log(name);
console.log(say);

то увидим «Иван», а переменная say не будет найдена. Вот так это работает. Причем, обратите внимание, свои лексические окружения создают не только вызываемые функции, но и условные операторы:

if(...) {
// внутреннее лексическое окружение
}

операторы циклов:

while, for, do while(...) {
// внутреннее лексическое окружение
}

И в современных браузерах даже просто проставленные фигурные скобки:

{
// внутреннее лексическое окружение     
}

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

(function() {
    // код внутри изолирован от внешнего скрипта
    let name = "Григорий";
    console.log(name);
})();

Причем, он исполнялся в момент, когда интерпретатор JavaScript-машины доходил до этого места программы. Но теперь, в этом приеме нет необходимости и можно просто использовать фигурные скобки.

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

Мы можем даже создавать функции внутри других функций. Они называются вложенными функциями. В JavaScript – это обычная практика:

function getName(say) {
    function getSay() {
         return ": " + say;
    }
 
    console.log(name + getSay());
}

Здесь функция getSay создана для удобства – вывода сообщения по определенному шаблону. Ее лексическое окружение будет вложено в лексическое окружение функции getName и, конечно же, она будет иметь доступ ко всем внутренним переменным функции getName.

Теперь, когда мы со всем этим разобрались, посмотрим на работу вот такой программы:

function createCounter() {
    let count = 0;
 
    return function() {
         return count++;
    };
}
 
let counter = createCounter();
console.log( counter() );

Вначале здесь объявляется функция createCounter, то есть, в глобальном окружении появляется такое свойство. Далее, эта функция вызывается. Создается ее локальное лексическое окружение с переменной count и анонимной функцией:

На эту анонимную функцию ссылается переменная counter из глобального окружения. В результате, после завершения выполнения функции createCounter() ее локальное окружение не уничтожается, так как все еще остается ссылка counter.

Далее, в программе идет вызов анонимной функции. Появляется еще одно лексическое пространство этой функции и картина выглядит уже вот так:

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

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

Но значение count теперь равно 1. Если мы вызовем counter() еще раз, то получим значение 1 и переменная count станет равной 2. И так далее. Вот так работают замыкания в JavaScript. Причем, обратите внимание, если мы создадим еще один счетчик:

let counter2 = createCounter();

то будет создано еще одно лексическое пространство и оба счетчика будут работать независимо друг от друга:

console.log( counter2() );

Я постарался предельно просто изложить материал о лексических окружениях и замыканиях, опуская некоторые несущественные на мой взгляд детали. Если вы хотите научиться грамотно писать код на JavaScript, то этот материал следует хорошо знать. Особенно, часто на собеседованиях соискателя любят спрашивать о замыканиях и особенностях работы лексических окружений. И теперь, вы все это знаете.

Видео по теме