Замыкание

Общее представление о замыканиях

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

Простое замыкание

var foo = 'Denis';

function fn(){
    console.log(foo); // Dendis
}

fn();

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

Чуть сложнее

var outerValue = 'Denis';
var later;

function outerFn(){
    var innerValue = 'Liza'; // объявить переменно в теле функции. Область видимости этой переменной ограничивается телоф функции, и поэтому она недоступна за пределами данной функции.
    function innerFn(){
        console.log(outerValue); // 1 утверждение
        console.log(innerValue); // 2 утверждение
    }

    later = innerFn; // сохранить ссылку на функция innerFn(). Эта переменная находится в глобальной области видимости, что дает возможность вызвать в дальнейшем данную функцию
}

outerFn(); // вызов данной функции приведет к созданию функции innerFn() и присвоению ссылки на нее переменной later
later(); // вызвать функцию innerFn() через переменную later. Ее нельзя вызвать непосредственно, так как область ее видимости (а также переменная innerValue) не выходит за пределы функции outerFn()

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

  • Первое утверждение должно пройти, поскольку переменная outerValue находится в глобальной области видимости и доступна повсюду.
  • Функция innerFn() выполняется после функции outerFn(), благодаря копирования ссылки на нее в глобальную ссылку, сохраняемую в переменой later.
  • Когда выполняется функция innerFn(), область видимости функции outerFn() уже покинута и недоступна на момент вызова этой функции по ссылке в переменной later.
  • Следовательно, можно было бы с полной уверенностью предположить, что второе утверждение не пройдет, поскольку значение переменной innerValue не определено (undefined), не так ли?

Тем не менее, тем проходит успешно и в консоль выводятся два имени.

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

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

В этом и состоит принцип действия замыкания. Они образуют "защитную оболочку" вокруг функции и переменных, находящихся в области ее видимости на момент объявления данной функции, и благодаря этому у нее имеется все необходимое для выполнения. Эта "оболочка", охватывающая как саму функцию, иак и ее переменные, остается до тех пок, пока существует сама функция.

И еще не большой пример, для закрепления

function fn() {
  var currentCount = 1;

  return function() {
    return currentCount++;
  };
}

var counter = fn();

// каждый вызов увеличивает счётчик и возвращает результат
console.log( counter() ); // 1
console.log( counter() ); // 2
console.log( counter() ); // 3

// создать другой счётчик, он будет независим от первого
var counter2 = fn();
console.log( counter2() ); // 1

Как видно, мы получили два независимых счётчика counter и counter2, каждый из которых незаметным снаружи образом сохраняет текущее количество вызовов.

Где? Конечно, во внешней переменной currentCount, которая у каждого счётчика своя.

Имитация закрытых переменных

Во многих языках программирования применяются закрытые переменные - свойства объекта, скрытые от внешнего мира. Это языковое средство удобно для того, чтобы не перегружать пользователей ненужными подробностями реализации при доступе к объектам из других частей кода. К сожалению, в JavaScript отсутствует собственная поддержка закрытых переменных. Но, используя понятие замыкания, можно добиться похожего результата.

function Fn(){
    var feints = 0; // объявить переменную в функции-конструкторе. Область видимости этой переменной ограничивается телом конструктора, поэтому она оказывается "закрытой" переменной. Она служит для подсчета количества значений.
    this.getFeints = function(){
        return feints;
    };
    this.feint = function(){
        feints++;
    };
}

var obj1 = new Fn();
obj1.feint(); // прибавляем на 1
console.log(obj1.feints); // проверить, можно ли получить прямой доступ к переменной (undefined)
console.log(obj1.getFeints()); // 1

var obj2 = new Fn();
console.log(obj2.getFeints()); // 0

Создается функция, служащая в качестве конструктора объектов типа Fn(). В самом конструкторе для хранения состояния определяется переменная feints. Правила соблюдения области видимости в JavaScript ограничивают доступность этой переменной в пределах конструктора. Чтобы получить доступ к значению данной переменной из кода за пределами области ее видимости, в рассматриваемом здесь коде определяется метод доступа getFeints(), с помощью которого можно только прочитать значение закрытой переменной. Как позабывают тесты а данном примере кода, с помощью метода доступа можно получить значение закрытой переменной, но не прямой доступ к ней.

Лексическое окружение

Все переменные внутри функции – это свойства специального внутреннего объекта LexicalEnvironment, который создаётся при её запуске.

Будем называть этот объект «лексическое окружение» или просто «объект переменных».

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

В отличие от window, объект LexicalEnvironment является внутренним, он скрыт от прямого доступа.

function sayHi(name) {
  var phrase = "Привет, " + name;
  alert( phrase );
}

sayHi('Вася');

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

  1. До выполнения первой строчки её кода, на стадии инициализации, интерпретатор создает пустой объект LexicalEnvironment и заполняет его. В данном случае туда попадает аргумент name и единственная переменная phrase:

     function sayHi(name) {
     // LexicalEnvironment = { name: 'Вася', phrase: undefined }
     var phrase = "Привет, " + name;
     alert( phrase );
     }
    
     sayHi('Вася');
    
  2. Функция выполняется. Во время выполнения происходит присвоение локальной переменной phrase, то есть, другими словами, присвоение свойству LexicalEnvironment.phrase нового значения:

     function sayHi(name) {
     // LexicalEnvironment = { name: 'Вася', phrase: undefined }
     var phrase = "Привет, " + name;
    
     // LexicalEnvironment = { name: 'Вася', phrase: 'Привет, Вася'}
     alert( phrase );
     }
    
     sayHi('Вася');
    
  3. В конце выполнения функции объект с переменными обычно выбрасывается и память очищается. В примерах выше так и происходит. Через некоторое время мы рассмотрим более сложные ситуации, при которых объект с переменными сохраняется и после завершения функции.

Из функции мы можем обратиться не только к локальной переменной, но и к внешней:

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

Такой порядок поиска возможен благодаря тому, что ссылка на внешний объект переменных хранится в специальном внутреннем свойстве функции, которое называется [[Scope]]. Это свойство закрыто от прямого доступа, но знание о нём очень важно для понимания того, как работает JavaScript.

При создании функция получает скрытое свойство [[Scope]], которое ссылается на лексическое окружение, в котором она была создана.

В примере выше таким окружением является window, так что создаётся свойство

sayHi.[[Scope]] = window

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

При запуске функции её объект переменных LexicalEnvironment получает ссылку на «внешнее лексическое окружение» со значением из [[Scope]].

Если переменная не найдена в функции – она будет искаться снаружи.

Если обобщить:

  • Каждая функция при создании получает ссылку [[Scope]] на объект с переменными, в контексте которого была создана.
  • При запуске функции создаётся новый объект с переменными LexicalEnvironment. Он получает ссылку на внешний объект переменных из [[Scope]].
  • При поиске переменных он осуществляется сначала в текущем объекте переменных, а потом – по этой ссылке.

Благодаря лексическому окружению и реализовано замыкание.

Что это такое – «понимать замыкания?»

«Понимать замыкания» в JavaScript означает понимать следующие вещи:

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