JavaScript - Что такое замыкание?
Урок, в котором рассмотрим что такое замыкание в 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('Вася')
нам будет возвращено "Привет, Вася!"
.

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

Изменим немного пример:
const message = 'Привет, ';
function sayHello() {
return function(name) {
return message + name + '!';
}
}
const result = sayHello(); // ƒ (name) { return message + name + '!'; }
console.log(result('Вася')); // "Привет, Вася!"
В этом примере выполнение result('Вася')
нам также вернёт "Привет, Вася!"
. Это произойдёт потому, что при поиске переменной message
, интерпретатор, будет переходить по ссылкам, от одного лексического окружения к другому,
начиная с текущего, пока не найдёт её. В данном случае он найдёт эту переменную в глобальном окружении.

Поиск переменной
Как же происходит поиск переменной? Поиск переменной всегда начинается с текущего лексического окружения. Т.е., если переменная будет сразу найдена в текущем лексическом окружении, то её дальнейший поиск прекратится и возвратится значение, которая эта переменная имеет здесь. Если искомая переменная в текущем окружении не будет найдена, то произойдёт переход к следующему окружению (ссылка на которое имеется в текущем). Если она не будет найдена в этом, то опять произойдёт переход к следующему окружению, и т.д. Если при поиске переменной, она будет найдена, то её дальнейший поиск прекратится и возвратится значение, которая она имеет здесь.
В качестве примера поместим константу 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
), то интерпретатор выдаст ошибку и
дальнейшее выполнение этого сценария прекратится.

Ещё один важный момент заключается в том, что лексические окружения создаются и изменяются в процессе выполнения кода. Рассмотрим это на следующем примере:
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(num2);
}
two(); // ?
Какой ответ мы получим в результате выполнения этого примера?
Пример №3:
const num2 = 3;
function one(num1) {
console.log(num1 + num2);
}
function two() {
const num2 = 20;
one(num2);
}
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);
На изображение видно, что внутренняя функция запомнила окружение, в котором была создана. Она имеет доступ к
переменной x
родительской функции. Значение данной переменной (x
) равно числу 2.
Теперь выведем в консоль значение функции c1(5)
:
console.log(c1(5));
Данная инструкция отобразит в консоли результат сложения значений параметров x
и y
.
Значение x
функция f2
будет брать из родительской области видимости.
Повторим вышепредставленные действия, но уже используя другую переменную (c2
):
var c2= f1(5);
console.dir(c2);
console.log(c2(5));

Представим переменные и функции рассмотренного примера для наглядности в виде следующей схемы:
Итоговый 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>
Если необходимо изменить при наступлении каких-то событий заголовок и содержимое модального окна (например, второго), то это будет выглядеть так:
JavaScriptmodal2('Другой заголовок','<p>Другое содержимое...</p>');
Комментарии: 10
Добрый день! Почему в пункте "Примеры для подробного рассмотрения лексического окружения и замыкания" в примерах 2 и 3 не указана переменная num1? из за этого не понятно решение или я чего то не понял
Привет! Да, там были перепутаны имена переменных. Поправил.
В чем разница между лексическим окружением и областью видимости?
«В JavaScript для очистки памяти используется автоматический сборщик мусора. Он, после того как функция завершает своё выполнение, просматривает «окружение», которое было создано при её запуске и если на него нет ссылок, то он его уничтожает. В вышеприведённом примере этого не произошло, т.к. переменная result, находящаяся в глобальной области видимости, содержит ссылку на функцию innerF.»
Но при запуске функции в запись окружения переменная result не должна ли попасть со значением этой родительской функции, то есть outerF? То есть получается, что result содержит ссылку на outerF… Или здесь имелось в виду, что после возврата функции, result станет содержать ссылку на innerF?
Да, здесь имеется ввиду результат выполнения функции outerF(3):
Но результатом outerF(3) является не просто какое-то значение, а ссылка на функцию innerF. Следовательно, в result находится ссылка на функцию innerF. Но так как в JavaScript функция «запоминает» или хранит ссылку на окружение, в котором оно было создано. То получается что функция innerF «запомнила» значения numA и numB, которые были внутри outerF(3) в момент её создания.
Вопросы по первому примеру.
1) Получается в первом примере переменной с1 присваивается ссылка на функцию f2, так как f1 возвращает f2?
2) и при каждом следующем вызове с1(4) будет вызываться функция f2 или f1?
Вопросы по второму примеру.
3) Как _count будет увеличиваться если функция incrementCount() не вызывается?
4) И каждый вызов countClickBtn1() будет вызывать эту функцию function(){return _count;}?
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.
Если вы вызываете просто функцию, то получить доступ к её окружению невозможно.
Например, вы никак не можете обратиться к переменной x, т.к. она будет существовать только в момент выполнения функций и после её завершения будет удалена сборщиком мусора автоматически. Это происходит из-за того что нет ссылки. А если ссылки нет, то значит, эти данные никому не нужны и они удаляются.
А если вы делаете замыкание, то у вас есть ссылка на родительский объект. В этом случае автоматический сборщик мусора его не удаляет, и вы тем самым сохраняете всё окружение, в котором она была создана.
Во втором примере надо было ещё добавить до return _count вызов приватной функции. Но сделал чуть-чуть по интересней и изменил немного описание.