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

Александр Мальцев
Александр Мальцев
17K
6
Содержание:
  1. Замыкание. Как оно работает
  2. Использование замыкания для создания приватных переменных и функций
  3. Примеры для подробного рассмотрения лексического окружения и замыкания
  4. JavaScript - Замыкание на примере
  5. Замыкания на практике
  6. Комментарии

Урок, в котором рассмотрим что такое замыкание в 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>');

Комментарии:

  1. nananana
    nananana
    26.04.2020, 23:58
    Здравствуйте, есть маленький вопрос по самому первому примеру. У вас написано:

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

    Но при запуске функции в запись окружения переменная result не должна ли попасть со значением этой родительской функции, то есть outerF? То есть получается, что result содержит ссылку на outerF… Или здесь имелось в виду, что после возврата функции, result станет содержать ссылку на innerF?

    1. Александр Мальцев
      Александр Мальцев
      27.04.2020, 15:27
      Здравствуйте!
      Да, здесь имеется ввиду результат выполнения функции outerF(3):
      var result = outerF(3);
      
      Но результатом outerF(3) является не просто какое-то значение, а ссылка на функцию innerF. Следовательно, в result находится ссылка на функцию innerF. Но так как в JavaScript функция «запоминает» или хранит ссылку на окружение, в котором оно было создано. То получается что функция innerF «запомнила» значения numA и numB, которые были внутри outerF(3) в момент её создания.
    2. Nariman
      Nariman
      21.10.2016, 16:17
      Супер, почти все понятно! Но есть несколько вопросов, буду благодарен если ответите.

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

      Вопросы по второму примеру.
      3) Как _count будет увеличиваться если функция incrementCount() не вызывается?
      4) И каждый вызов countClickBtn1() будет вызывать эту функцию function(){return _count;}?
      1. Александр Мальцев
        Александр Мальцев
        22.10.2016, 13:04
        Ответ на первую часть вопроса.
        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 вызов приватной функции. Но сделал чуть-чуть по интересней и изменил немного описание.
      2. Valery Semenencko
        Valery Semenencko
        08.07.2016, 15:30
        Красавчик! Очень хорошо и красочно все описал! Подписался не зря на твой ресурс )
        1. Александр Мальцев
          Александр Мальцев
          10.07.2016, 12:17
          Спасибо за отзыв.
        Войдите, пожалуйста, в аккаунт, чтобы оставить комментарий.