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

Замыкание. Как оно работает?

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

Замыкание - это такой механизм в JavaScript, который даёт нам доступ к переменным внешней функции из внутренней.

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

function sayHello() {
  const message = 'Привет, ';
  return function(name) {
    return message + name + '!';
  }
}

const result = sayHello(); // ƒ (name) { return message + name + '!'; }
console.log(result('Вася')); // "Привет, Вася!"

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

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

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

В этом примере будет создано 3 лексических окружения: внешнее (глобальное) и 2 внутренних (первое - при вызове функции sayHello() и второе - при result('Вася')):

  • Глобальное лексическое окружение (1) будет создано самим скриптом, в нём будет находиться функция sayHello и константа result. У глобального окружения нет внешнего окружения (ссылка на внешнее окружение равна null).
  • Одно внутреннее лексическое окружение (2) будет создано при вызове функции sayHello, которая нам в качестве результата возвратит другую функцию (её мы сохраним в константу result). В этом лексическом окружении (2) будет находиться переменная message со значением "Привет, ", и ссылка на внешнее (глобальное) окружение (1).
  • Другое внутреннее лексическое окружение (3) соответствует вызову result('Вася'). В нём находится одна переменная name со значением 'Вася' и ссылка на внешнее лексическое окружение (2), т.к. в JavaScript функция «запоминает» то место, в котором она была создана.

Таким образом, когда мы вызываем result('Вася'), то создаётся лексическое окружение (3), в котором находится не только name со значением "Вася", но и ссылка на внешнее окружение (2). Это внешнее окружение (2) было создано при запуске функции sayHello. Оно содержит переменную message со значением "Привет, ". Не смотря на то, что функция sayHello уже выполнилась, её лексическое окружение (2) нам доступно, т.к. у нас есть ссылка на него. А т.к. в лексическом окружении (3) нет переменной message, то оно будет искаться в следующем окружении, на которое указывает текущее. Т.е. в лексическом окружении (2). В этом окружении оно есть. Таким образом, в результате выполнения result('Вася') нам будет возвращено "Привет, Вася!".

Что такое лексическое окружение в JavaScript

Если после console.log(result('Вася')) мы поместим ещё один вызов функции result, то для него создастся лексическое окружение (4), которое то же будет иметь ссылку на внешнего окружение (2). Но, так как в лексическом окружение (4) переменной message нет, то оно будет взято из окружения (2). В результате, нам в консоль будет выведено "Привет, Петя!":

console.log(result('Петя')); // "Привет, Петя!"
Лексическое окружение в JavaScript

Изменим немного пример:

const message = 'Привет, ';
function sayHello() {
  return function(name) {
    return message + name + '!';
  }
}

const result = sayHello(); // ƒ (name) { return message + name + '!'; }
console.log(result('Вася')); // "Привет, Вася!"

В этом примере выполнение result('Вася') нам также вернёт "Привет, Вася!". Это произойдёт потому, что при поиске переменной message, интерпретатор, будет переходить по ссылкам, от одного лексического окружения к другому, начиная с текущего, пока не найдёт её. В данном случае он найдёт эту переменную в глобальном окружении.

Поиск переменной в коде JavaScript

Поиск переменной

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

В качестве примера поместим константу message в другую функцию:

function getMessage() {
  const message = 'Привет, ';
  return message;
}
function sayHello() {
  return function(name) {
    return message + name + '!'; // Uncaught ReferenceError: message is not defined
  }
}

console.log(getMessage());
const result = sayHello(); // ƒ (name) { return message + name + '!'; }
// произойдёт ошибка когда мы вызовем функцию result('Вася')
console.log(result('Вася'));

В этом примере произойдёт ошибка, т.к. переменная message не будет найдена. Интерпретатор при её поиске перейдёт от текущего лексического окружения по ссылкам до глобального. А т.к. в нём этой переменной нет и ссылки на следующее окружение тоже (она равна null), то интерпретатор выдаст ошибку и дальнейшее выполнение этого сценария прекратится.

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

Ещё один важный момент заключается в том, что лексические окружения создаются и изменяются в процессе выполнения кода. Рассмотрим это на следующем примере:

function sayHello() {
  return function(name) {
    return message + name + '!';
  }
}

const result = sayHello(); // ƒ (name) { return message + name + '!'; }
let message = 'Привет, ';
console.log(result('Вася')); // "Привет, Вася!"
message = 'Здравствуйте, ';
console.log(result('Вася')); // "Здравствуйте, Вася!"

В этом примере, когда мы первый раз вызываем функцию result('Вася'), в глобальном лексическом окружении переменная message имеет значение 'Привет, '. В результате мы получим строку "Привет, Вася!". При втором вызове переменная message имеет уже значение 'Здравствуйте, '. В результате мы уже получим строку "Здравствуйте, Вася!".

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

Сборка мусора

В JavaScript лексическое окружение обычно удаляется после того, как функция выполнилась. Это происходит только тогда, когда у нас нет ссылок на это окружение. Как например, в этом примере:

function sayHello(name) {
  return 'Привет, ' + name + '!';
}

console.log(sayHello('Вася')); // "Привет, Вася!"

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

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

Использование замыкания для создания приватных переменных и функций

Замыкания в JavaScript можно использовать для создания приватных переменных и функций.

const counter = () => {
  // приватная переменная _counter
  let _counter = 0;
  // приватная функция _changeBy (изменяет значение переменой _counter на переданное ей значение в качестве аргумента)
  const _changeBy = (value) => {
    _counter += value;
  };
  // возвращаемое значение функции (объект, состоящий из 3 методов)
  return {
    // публичный метод (функция) increment (для увеличения счетчика на 1)
    increment() {
      _changeBy(1);
    },
    // публичный метод (функция) decrement (для уменьшения счетчика на 1)
    decrement() {
      _changeBy(-1);
    },
    // публичный метод (функция) value (для получения текущего значения _counter)
    value() {
      return _counter;
    },
  };
};

// создадим счетчик 1
const counter1 = counter();
// создадим счетчик 2
const counter2 = counter();

counter1.increment();
counter1.increment();
console.log(counter1.value()); // 2
counter1.decrement();
console.log(counter1.value()); // 1

counter2.decrement();
counter2.decrement();
console.log(counter2.value()); // -2

Напрямую обратиться к _counter и _changeBy нельзя.

console.log(counter1._counter); // undefined
counter1._changeBy(1); // Uncaught TypeError: counter1._changeBy is not a function

Обратиться к ним можно только через функции increment, decrement и value.

Примеры для подробного рассмотрения лексического окружения и замыкания

Пример №1:

function one() {
  console.log(num); // Uncaught ReferenceError: num is not defined
}
function two() {
  const num = 5;
  one();
}
two();

В этом примере мы получим ошибку. Т.к. функция one имеет в качестве внешнего окружения глобальное, и, следовательно, не может получить доступ к переменной num даже не смотря на то, что вызываем мы её внутри функции two.

Пример №2:

function one(num1) {
  console.log(num1 + num2);
}
function two() {
  const num2 = 20;
  one(num1);
}
two();  // ?

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

Пример №3:

const num2 = 3;
function one(num1) {
  console.log(num1 + num2);
}
function two() {
  const num2 = 20;
  one(num1);
}
two();  // ?

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

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>');