Статья в доработке!

Урок, в котором рассмотрим что такое замыкание в JavaScript и зачем оно нужно. После этого выполним несколько практических примеров. В первом примере разберём, как происходит замыкание, а во втором - некоторую реальную задачу с использованием front-end фреймворка Bootstrap. В конце урока познакомимся с тем, как можно использовать замыкания для создания приватных переменных и функций.

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

В JavaScript функции могут находиться внутри других функций.

Когда одна функция находится внутри другой, то внутренняя функция имеет доступ к области видимости (окружению) внешней функции.

Этот способ организации кода в JavaScript позволяет создавать замыкания.

Рассмотрим пример создания замыкания:

function outerF(numA) {
  var numB = 5;
  function innerF(numC) {
    return numA + numB + numC;
  }
  return innerF;
}

var result = outerF(3);
console.log(result(7)); // 15
console.log(result(10)); // 18      
JavaScript - Пример замыкания

В переменной result будет находиться результат выполнения функции outerF(3), т.е. ссылка на функцию innerF.

Функции в JavaScript «запоминают» окружение, в котором они были созданы. Осуществляют это они посредством скрытого свойства [[Scope]].

В этом примере функция innerF «запомнит» своё окружение, т.е. она в своём скрытом свойстве [[Scope]] будет содержать ссылку на область видимости, в которой она была создана, в данном случае это outerF(3).

В результате получилось замыкание (closure), т.е. такое состояние в котором некоторая функция (в примере это innerF) имеет доступ к внешнему окружению, в данном случае другой функции outerF(3), которая завершило уже своё выполнение.

Почему так происходит? В JavaScript для очистки памяти используется автоматический сборщик мусора. Он, после того как функция завершает своё выполнение, просматривает «окружение», которое было создано при её запуске и если на него нет ссылок, то он его уничтожает. В вышеприведённом примере этого не произошло, т.к. переменная result, находящаяся в глобальной области видимости, содержит ссылку на функцию innerF. А функция innerF содержит ссылку посредством своего скрытого свойства [[Scope]] на «окружение» outerF(3). В результате «окружение» outetF(3) не может быть удалено автоматическим сборщиком мусора даже несмотря на то, что функция уже завершила своё выполнение. Это происходит из-за того, что на «окружение» outerF(3) существует [[Scope]] ссылка в функции innerF, а ссылка на innerF имеется в переменной result, находящейся в глобальной области видимости.

Для чего нужны замыкания? Замыкания, например, могут использоваться для «запоминания» параметров, защиты данных (инкапсуляции), привязывания функции к определённому контексту и др. Замыкания положены в основу многих паттернов (шаблонов для написания кода).

JavaScript - Замыкание на примере

Рассмотрим на примере, как происходит замыкание в JavaScript.

Объявим некоторую функцию, например f1. Внутри этой функции объявим ещё одну функцию f2 (внутреннюю) и вернём её в качестве результата первой. Функция f1 пусть имеет параметр (переменную) x, а функция f2 - параметр (переменную) y. Функция f2 кроме доступа к параметру x имеет ещё доступ и к параметру y (по цепочки областей видимости).

//родительская функция для f2
function f1(x) {
  //внутренняя функция f2 по отношению к f1 
  function f2(y) {
    return x + y;
  }
  //родительская функция возвращает в качестве результата внутреннюю функцию
  return f2;
}

Теперь перейдём к самому интересному, а именно рассмотрим, что произойдёт, если некоторой переменной c1 присвоить вызов функции f1(2).

var c1 = f1(2);

В результате выполнения функция f1(2) вернёт другую (внутреннюю) функцию f2. Но, функция f2 в данном контексте позволяет получить значения переменных родительской функции (f1) даже несмотря на то, что функция f1 уже завершила своё выполнение.

Посмотрим детальную информацию о функции:

console.dir(c1);

JavaScript - Информация о функции c1 (console.dir)

На изображение видно, что внутренняя функция запомнила окружение, в котором была создана. Она имеет доступ к переменной x родительской функции. Значение данной переменной (x) равно числу 2.

Теперь выведем в консоль значение функции c1(5):

console.log(c1(5));

Данная инструкция отобразит в консоли результат сложения значений параметров x и y. Значение x функция f2 будет брать из родительской области видимости.

JavaScript - Вывод в консоль значения функции c1(5)

Повторим вышепредставленные действия, но уже используя другую переменную (c2):

var c2= f1(5);
console.dir(c2);
console.log(c2(5));

JavaScript - Вывод информации о функции c2 и значения функции c2(5) в консоль

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

JavaScript - Переменные и функции вышепредставленного примера в виде схемы

Итоговый js-код рассмотренного примера:

//родительская функция
function f1(x) {
  //внутренняя функция f2 
  function f2(y) {
    return x + y;
  }
  //родительская функция возвращает в качестве результата внутреннюю функцию
  return f2;
}
var c1 = f1(2);
var c2 = f1(5);
//отобразим детальную информацию о функции c1
console.dir(c1);
//отобразим детальную информацию о функции c2
console.dir(c2);
console.log(c1(5)); //7
console.log(c2(5)); //10

Замыкания на практике

Замыкания в JavaScript являются очень интересной вещью. Они позволяют связать некоторые данные с функцией. Это очень похоже на то, как это реализовано в объекте, который позволяет связать свойства (переменные) и методы (действия над этими переменными). Такие задачи в веб-разработке попадаются очень часто. Давайте рассмотрим одну из подобных задач.

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

Кнопки, открывающие модальные окна:

<button type="button" id="myButton1" class="btn btn-primary">Кнопка 1</button>
<button type="button" id="myButton2" class="btn btn-primary">Кнопка 2</button>
<button type="button" id="myButton3" class="btn btn-primary">Кнопка 3</button>

Функция, возвращая в качестве результата другую функцию:

function modalContent(idModal,idButton){
  //переменная, содержащая код модального окна Bootstrap
  var modal='<div id="'+idModal+'" class="modal fade" tabindex="-1" role="dialog">'+
    '<div class="modal-dialog"><div class="modal-content">'+
    '<div class="modal-header">'+
    '<button type="button" class="close" data-dismiss="modal" aria-label="Close">'+
    '<span aria-hidden="true">×</span></button>'+
    '<h4 class="modal-title"></h4></div><div class="modal-body"></div>'+
    '<div class="modal-footer">'+
    '<button type="button" class="btn btn-default" data-dismiss="modal">Закрыть</button>'+
    '</div></div></div></div>';
  //инструкция, добавляющая HTML-код модального окна сразу после открывающего тега body
  $(modal).prependTo('body');
  //связываем модальное окно с кнопкой:
  $('#'+idButton).click(function(){
    $('#'+idModal).modal('show');
  });
  // функция modalContent возвращает в качестве результата другую функцию
  return function(modalTitle,modalBody) {
    //устанавливаем заголовок модальному окну
    $('#'+idModal).find('.modal-title').html(modalTitle);
    //устанавливаем модальному окну содержимое
    $('#'+idModal).find('.modal-body').html(modalBody);
  } 
}

Код, который выполняет создание модальных окон и установлением каждому из них заголовка и некоторого содержимого:

$(function(){
  //1 модальное окно
  var modal1 = modalContent('modal1','myButton1');
  modal1('Заголовок 1','<p>Содержимое 1...</p>');
  //2 модальное окно
  var modal2 = modalContent('modal2','myButton2');
  modal2('Заголовок 2','<p>Содержимое 2...</p>');
  //3 модальное окно
  var modal3 = modalContent('modal3','myButton3');
  modal3('Заголовок 3','<p>Содержимое 3...</p>');
});

Итоговый код (кнопки + скрипт):

<script>
function modalContent(idModal,idButton){
  var modal='<div id="'+idModal+'" class="modal fade" tabindex="-1" role="dialog">'+
    '<div class="modal-dialog"><div class="modal-content">'+
    '<div class="modal-header">'+
    '<button type="button" class="close" data-dismiss="modal" aria-label="Close">'+
    '<span aria-hidden="true">×</span></button>'+
    '<h4 class="modal-title"></h4></div><div class="modal-body"></div>'+
    '<div class="modal-footer">'+
    '<button type="button" class="btn btn-default" data-dismiss="modal">Закрыть</button>'+
    '</div></div></div></div>';
  $(modal).prependTo('body');
  $('#'+idButton).click(function(){
    $('#'+idModal).modal('show');
  });
  return function(modalTitle,modalBody) {
    $('#'+idModal).find('.modal-title').html(modalTitle);
    $('#'+idModal).find('.modal-body').html(modalBody);
  } 
}
$(function(){
  //1 модальное окно
  var modal1 = modalContent('modal1','myButton1');
  modal1('Заголовок 1','<p>Содержимое 1...</p>');
  //2 модальное окно
  var modal2 = modalContent('modal2','myButton2');
  modal2('Заголовок 2','<p>Содержимое 2...</p>');
  //3 модальное окно
  var modal3 = modalContent('modal3','myButton3');
  modal3('Заголовок 3','<p>Содержимое 3...</p>');
});
</script>

<button type="button" id="myButton1" class="btn btn-primary">Кнопка 1</button>
<button type="button" id="myButton2" class="btn btn-primary">Кнопка 2</button>
<button type="button" id="myButton3" class="btn btn-primary">Кнопка 3</button>

Если необходимо изменить при наступлении каких-то событий заголовок и содержимое модального окна (например, второго), то это будет выглядеть так:

modal2('Другой заголовок','<p>Другое содержимое...</p>');

Создание приватных методов посредством замыканий

Замыкания в JavaScript применяются также для создания приватных переменных и функций (методов). Это необходимо для того, чтобы ограничить доступ к переменным и функциям (методам). Данный вариант - это также отличный способ для того чтобы создать своё пространство имён. Оно позволит защитить созданные переменные от их изменения посредством глобальных переменных.

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

HTML-код кнопок:

<button type="button" id="btn1" class="btn btn-primary">Кнопка 1</button>
<button type="button" id="btn2" class="btn btn-primary">Кнопка 2</button>

Функция, имеет приватную переменную _count и функцию (метод) incrementCount. Для управления приватными методами предназначены публичные методы (increment() и value), которая данная функция возвращает в качестве результата.

var countButtonClick = function () {
  //приватная переменная 
  var _count = 0;
  //приватная функция (увеличивает значение на 1)
  function incrementCount() {
    _count++;
  }
  //результат, который возвращает функция в результате своего выполнения 
  return {
    increment: function() {
      incrementCount();
    },
    value: function() {
      return _count;
    }
  }
}

Напишем JavaScript сценарий, который после загрузки страницы повесит обработчик события click на каждую из кнопок. В обработчиках кнопок будет вызывать публичные методы замкнутой функции, один из которых будет увеличивать переменную _count в соответствующем окружении на 1, а второй возвращать её значение.

$(function(){
  var countClickBtn1 = countButtonClick();
  var countClickBtn2 = countButtonClick();
  $('#btn1').click(function(){
    countClickBtn1.increment()
    console.log(countClickBtn1.value());
  });
  $('#btn2').click(function(){
    countClickBtn2.increment()
    console.log(countClickBtn2.value());
  });
});

Итоговый код:

<button type="button" id="btn1" class="btn btn-primary">Кнопка 1</button>
<button type="button" id="btn2" class="btn btn-primary">Кнопка 2</button>
<script>
var countButtonClick = function () {
  //приватная переменная 
  var _count = 0;
  //приватная функция (увеличивает значение на 1)
  function incrementCount() {
    _count++;
  }
  //результат, который возвращает функция в результате своего выполнения 
  return {
    increment: function() {
      incrementCount();
    },
    value: function() {
      return _count;
    }
  }
}
$(function(){
  var countClickBtn1 = countButtonClick();
  var countClickBtn2 = countButtonClick();
  $('#btn1').click(function(){
    countClickBtn1.increment()
    console.log(countClickBtn1.value());
  });
  $('#btn2').click(function(){
    countClickBtn2.increment()
    console.log(countClickBtn2.value());
  });
});
</script>