Замыкание функции в JavaScript

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

Что такое замыкание в JavaScript?

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

Почему так происходит? В JavaScript для очистки памяти используется автоматический сборщик мусора. Т.е. после того как функция отработала, все внутренние переменные, которые были созданы в момент её выполнения автоматически уничтожаются сборщиком мусора и получить доступ к ним уже нельзя. А что если внутри некоторой функции поместить ещё одну функцию и вернуть её в качестве результата. В этом случае случится замыкание, т.к. в JavaScript любая функция имеет ссылку на родительский объект (в данном случае на переменные родительской функции). А пока существует ссылка, сборщик мусора не может уничтожить переменные родительской функции и к ним можно обратиться. Такой приём в JavaScript позволяет внутренней функции сохранять окружение, в котором она создана.

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>


   JavaScript и jQuery 0    1856 0

Комментарии (4)

  1. Valery Semenencko # 0
    Красавчик! Очень хорошо и красочно все описал! Подписался не зря на твой ресурс )
    1. Александр Мальцев # 0
      Спасибо за отзыв.
    2. Nariman # 0
      Супер, почти все понятно! Но есть несколько вопросов, буду благодарен если ответите.

      Вопросы по первому примеру.
      1) Получается в первом примере переменной с1 присваивается ссылка на функцию f2, так как f1 возвращает f2?
      2) и при каждом следующем вызове с1(4) будет вызываться функция f2 или f1?

      Вопросы по второму примеру.
      3) Как _count будет увеличиваться если функция incrementCount() не вызывается?
      4) И каждый вызов countClickBtn1() будет вызывать эту функцию function(){return _count;}?
      1. Александр Мальцев # 0
        Ответ на первую часть вопроса.
        1) В переменной c1 будет результат выполнения функции f1 (в данном случае ссылка на функцию f2). А т.к. функция f2 расположена в f1, то она (f2) будет содержать ссылку на окружение функции f1. Т.е. она запомнит окружение, в котором была создана.
        2) При вызове c1(4) функция f1 не может быть вызвана, т.к. её уже нет. Остался только результат её выполнения (f2). Т.е. c2(4) как бы равно f2(4), но с учётом окружения, в котором она была создана. Т.е. когда вызываем c2(4), нам должен вернуться результат x + y. 4 — это параметр y, а параметра x — нет. А т.к. переменной x нет, то она пытается его найти в родительском окружении (посредством ссылки). А в родительском окружении (var c1 = f1(2)), данное значение равно 2.

        Ещё один момент, который может поможет понять замыкания в JavaScript.
        Если вы вызываете просто функцию, то получить доступ к её окружению невозможно.
        function f10(y) {
          var x = 2;
          return x+y;
        }
        // т.е. получаете просто результат, без возможности получения доступа к окружению (переменной x) 
        var c10 = f10(5); //7
        
        Например, вы никак не можете обратиться к переменной x, т.к. она будет существовать только в момент выполнения функций и после её завершения будет удалена сборщиком мусора автоматически. Это происходит из-за того что нет ссылки. А если ссылки нет, то значит, эти данные никому не нужны и они удаляются.

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

        Во втором примере надо было ещё добавить до return _count вызов приватной функции. Но сделал чуть-чуть по интересней и изменил немного описание.

      Вы должны авторизоваться, чтобы оставлять комментарии.