Позвольте пропустить дежурные фразы о популярности JavaScript, о его мультипарадигменности и гибкости, о растущем количестве нововведений в стандарте ECMAScript (нужное подчеркнуть). Да и о том, что замыкания — это «мощный инструмент», который не все умеют эффективно использовать, тоже писать не будем. Итак, меньше слов — больше кода:
function sayHello() { var name = 'John'; // name — локальная переменная, объявленная и проинициализированная внутри функции sayHello() function say() { // say() — функция, использующая переменную из внешней функции sayHello() console.log('Hello, ' + name); } say(); } sayHello();
Заметьте, что здесь внутри sayHello()
вызывается функция say()
.
Программа выдает строку:
Hello, John
Теперь внесем небольшие изменения в код:
function makeFunc() { var name = 'John'; function say() { console.log('Hello, ' + name); } return say; // мы не вызываем, а просто возвращаем функцию } var myFunc = makeFunc(); myFunc();
Здесь мы стали свидетелями так называемого возврата функции из функции (более подробно речь об этом пойдет ниже). Более того, в отличие от предыдущего примера, вызов этой функции (речь о say()
) происходит только в последней строке (с помощью myFunc()
).
Функция say()
вызывается из другого блока кода, но использует переменную name
из внешней функции myFunc()
, которая к этому моменту уже завершила свою работу. Будет ли этот пример работать корректно?
Мы прощаемся с myFunc()
и вроде бы должны проститься с переменной name
. Она объявлена как var
, а это значит, что name
живет только внутри этой функции. Но нет, она будет вечно жить в наших сердцах!
Тем не менее, этот код выдает тот же результат, что и в предыдущем примере.
Пример работает корректно: программа выдает строку:
Hello, John
Возможно, кому-то этот код покажется немного запутанным. Но в JS это работает. И виновато в этом не искривление пространства и времени, а более предсказуемое «явление» — замыкание.
Замыкание — механизм, позволяющий внутренним функциям использовать и изменять переменные внешней функции (даже после окончания ее работы).
Получается, что после завершения работы внешней функции makeFunc()
ее переменная name
продолжает храниться в каком-то другом месте. И функция say()
из второго примера способна в любой момент получить туда доступ, прочитать значение этой переменной и даже изменить его.
Давайте разберемся, как и почему это работает, рассмотрим больше примеров, поговорим о том, как и где можно использовать замыкания. Ну и, конечно, в конце усвоим народную мудрость про сборку мусора.
Лексическое окружение — это объект, в котором хранятся:
Скрипт — это блок кода для глобальных переменных:
Переменную можно считать одним из свойств объекта лексического окружения. При изменении переменной меняется и соответствующее свойство объекта.
В этом случае мы находимся в глобальном лексическом окружении (внутри скрипта нет функций и блоков кода), и свойство «внешнее лексическое окружение» ссылается на null
.
Вот что произойдет с нашим объектом лексического окружения при запуске кода:
phrase
еще не объявлена, но объект с помощью лексического анализатора прочитал код и заранее «знает» о ней;phrase
ее свойство примет значение undefined;
phrase
примет значение Hello
.phrase
примет значение Bye
.Пока все очень просто.
Здесь вся необходимая информация о функции заносится в объект лексического окружения еще до того, как выполнится строка кода с ее объявлением. В нашем примере это происходит с функцией say(name)
. И во время выполнения кода значение свойства say
уже не меняется.
Это принципиально отличается от того, как объект лексического окружения обрабатывает переменные. Запомните эту интересную особенность хранения информации о функциях. Она нам еще пригодится.
И что же произойдет, когда мы вызовем нашу функцию, например, с аргументом John
?
В момент вызова функции в игру вступает ее тело. Это блок кода, имеющий свое собственное (внутреннее) лексическое окружение. Поэтому, пока функция (а точнее — тело) не завершит работу, мы будем иметь дело с двумя лексическими окружениями — внутренним и внешним.
Внутри блока есть alert
, который использует две переменные — name
и phrase
. С переменной name
все просто: она находится в лексическом окружении нашего блока (тела функции), которое мы назвали внутренним. Но переменной phrase
в этом окружении нет.
Вот тут нам и понадобится ссылка на внешнее окружение, которое находится на более высоком уровне в цепочке окружений. В этом случае внешним окружением мы назвали глобальное окружение, в котором как раз находятся переменная phrase
и сама функция say
.
Так и работает алгоритм поиска переменных:
1. ищет в текущем окружении (в нашем примере оно внутреннее — это тело функции say
);
2. если находит:
а) прекращает искать;
б) использует переменную;
3. если не находит:
а) по ссылке перемещается во внешнее окружение (делает его текущим);
б) переходит к пункту 1.
4. Если ничего не находит ни в одном окружении, то в зависимости от режима — либо выдает ошибку ( в режиме strict), либо создает новую глобальную переменную (в обычном режиме).
Мы уже использовали это выражение выше. Настало время объяснить подробнее. Взгляните на код и схему:
В этом случае будет минимум два лексических окружения — внешнее и внутреннее. Это понятно по прошлому примеру. Но здесь еще есть безымянная (правильнее ее называть «анонимной») функция, которую возвращает makeCounter()
. При этом она не будет запущена до тех пор, пока мы не запишем ее в переменную и не сделаем вызов явно. В этом примере это можно сделать так:
let counter = makeCounter(); alert( counter() );
Чтобы продвинуться дальше, мы должны знать, что все функции (включая анонимные) сохраняют свое внутреннее лексическое окружение в скрытом свойстве [[Environment]]
.
Вооружившись этой информацией, перерисуем схему:
Это специальный объект, созданный специально для хранения окружений. В нашем примере [[Environment]]
нашей безымянной функции почти пуст (<empty>
). У него есть лишь ссылка на ее внешнее лексическое окружение.
Теперь, вызывая нашу функцию в последней строке, изменим значение переменной count
:
Вызывая функцию counter()
и многократно выполняя count++
внутри нее, мы каждый раз будем получать новое значение — 1, 2, 3, 4 и так далее.
Состояние переменной count
каждый раз обновляется и хранится в объекте внешнего лексического окружения нашей безымянной функции. А получить к нему доступ и модифицировать состояние переменной она может благодаря ссылке, хранящейся в ее объекте [[Environment]]
.
Скорее всего, вам уже и так понятно, что «вложенными» бывают не только лексические окружения, но и функции.
function sayHiBye(firstName, lastName) { /* Ниже две вспомогательные функции. В них заданы способы приветствия и прощания */ function getFirstName() { return firstName; } function getLastName() { return lastName; } console.log( "Hello, " + getFirstName()); console.log( "Bye, " + getLastName() ); } sayHiBye("John", "Petrov");
Пример выше показывает, что внутри одной функции можно реализовать несколько вложенных функций.
Результат работы будет таким:
"Hello, John" "Bye, Petrov"
В следующем примере мы повысили вложенность:
getLastName()
;addMr()
.function sayHiBye(firstName, lastName) { /* Ниже две вспомогательные функции. В них заданы способы приветствия и прощания */ function getFirstName() { return firstName; } function getRespect(lName) { /* эта функция вложена еще глубже, она вспомогательная для вспомогательной */ function addMr() { return "Mr. " + lName; } var mrLastName = addMr(); return mrLastName; } console.log( "Hello, " + getFirstName()); console.log( "Bye, " + getRespect(lastName) ); } sayHiBye("John", "Petrov");
Результат, который у нас получится:
"Hello, John" "Bye, Mr, Petrov"
Но будет ли работать код в следующем примере? Теперь дважды вложенная функция addMr()
должна где-то найти переменную lastName
. Ведь мы больше не передаем ее в качестве параметра из функции getRespect()
.
function sayHiBye(firstName, lastName) { /* Ниже две вспомогательные функции. В них заданы способы приветствия и прощания */ function getFirstName() { return firstName; } function getRespect() { /* эта функция вложена еще глубже, она вспомогательная для вспомогательной */ function addMr() { return "Mr. " + lastName; } var mrLastName = addMr(); return mrLastName; } console.log( "Hello, " + getFirstName() ); console.log( "Bye, " + getRespect() ); } sayHiBye("John", "Petrov");
Если код все-таки выполнится корректно, то подумайте, пожалуйста, самостоятельно: есть ли в этом коде замыкания?
var makeCounter = function() { var privateCounter = 0; function changeBy(val) { privateCounter += val; } return { increment: function() { changeBy(1); }, decrement: function() { changeBy(-1); }, value: function() { return privateCounter; } } }; var counter = makeCounter(); alert(counter.value()); // 0. counter.increment(); counter.increment(); alert(counter.value()); // 2. counter.decrement(); alert(counter.value()); // 1.
В этом примере можно наблюдать интересный эффект: попытка выполнить counter.changeBy(2)
приводит к ошибке.
Выражаясь языком ООП, функция changeBy(val)
является как бы приватной (private). Это значит, что к ней нельзя получить доступ за пределами ее внешней функции makeCounter()
.
А три функции counter.increment
, counter.decrement
и counter.value
наоборот являются как бы публичными (public). И более того: они имеют общее лексическое окружение, заключенное в блоке после ключевого слова return. Да, и так тоже можно!
Убедитесь в этом сами и попробуйте в конце добавить counter.changeBy(2)
.
В JS через замыкания можно реализовать так называемые callback-функции. Такие функции должны быть запущены как раз после того, как отработает некоторая внешняя функция. При этом callback-функция должна быть определена внутри этой внешней функции и иметь доступ к ее переменным после завершения работы. Пока все сходится.
Рассмотрим пример:
function sendAjax(){ var outerVar = "abc"; $.ajax({ cache : ..., url : ..., type : ..., crossDomain: ..., data : ...., contentType : ..., timeout : 20000, dataType : ..., success : function(response){ console.log(outerVar); }, error : function(jXhr, err){ console.log(outerVar); }, xhrFields: { withCredentials: true } }); }
Здесь sendAjax()
— та самая внешняя функция. Внутри нее определена функция, которая запускается, если процесс отправки Ajax-запроса завершился успешно (success). Эта callback-функция использует внешнюю переменную outerVar:
console.log(outerVar);
Что и требовалось доказать.
При обработке событий, связанных с действиями пользователя (например, клик мышью или нажатие кнопки клавиатуры), мы часто меняем свойства элементов DOM. В этом нам также помогают замыкания.
Допустим, нужно устанавливать новый размер шрифта по нажатию кнопок, расположенных на HTML-странице:
Напишем функцию makeSizer(size)
, которая будет реагировать на соответствующие события для каждой кнопки:
function makeSizer(size) { return function() { document.body.style.fontSize = size + 'px'; }; }
Внутри нее реализуем замыкание. В данном случае это анонимная функция, которая для изменения размера шрифта использует внешнюю переменную size
. И будем возвращать эту анонимную функцию как результат работы внешней функции makeSizer(size)
:
var size12 = makeSizer(12); var size14 = makeSizer(14); var size16 = makeSizer(16);
Тогда size12
, size14
и size16
— это отдельные функции, которые устанавливают размер шрифта страницы равным 12, 14 и 16 пикселей соответственно.
Если хотите поиграться с кодом и программой, заходите сюда.
Я уже писал, что после завершения работы внешней функции и ее удаления из памяти внутренняя (вложенная) функция продолжает хранить ее переменные в своем скрытом свойстве [[Environment]]
.
В JS, как и во многих других языках, очисткой памяти занимается Сборщик мусора. Как и когда удаляются из памяти переменные внешней функции в нашей ситуации?
function f() { let value = 123; return function() { alert(value); } } let g = f(); // g.[[Environment]] хранит ссылку на объект лексического окружения функции f()
В приведенном выше примере в g.[[Environment]]
сохраняется переменная value
и остается там даже после того, как мы вызовем функцию f()
и она завершит работу. Хотя известно, что переменные, объявленные как let
, должны уничтожаться при выходе из блока. Должны, да не обязаны.
Более сложный пример лучше показывает масштаб бедствия: все в три раза хуже. У нас теперь появляются целых три копии переменной value
. Каждый раз при вызове функции f()
выделяется и не очищается память под новую копию лексического окружения (читай: новый объект [[Environment]]
):
function f() { let value = Math.random(); return function() { alert(value); }; } /* Создадим массив из трех функций f(). В этом случае каждый экземпляр функции (напоминаю, в JS функция — это тоже объект) хранит свою копию объекта лексического окружения */ let arr = [f(), f(), f()];
Ну что же, тогда мы будем самостоятельно следить за распределением и очисткой памяти в подобных ситуациях. Когда нам становится не нужен объект g.[[Environment]]
и в частности все переменные внешнего лексического окружения, скажем об этом прямо:
g = null;
В принципе, все. Только не забудьте сделать так, чтобы эта очистка памяти происходила в нужном месте и в нужное время.
function f() { let value = 123; return function() { alert(value); } } let g = f(); // пока в переменной g хранится функция f() — переменная value существует g = null; // упс… уже не существует
Прокси (proxy), или прокси-сервер — это программа-посредник, которая обеспечивает соединение между пользователем и интернет-ресурсом. Принцип…
Согласитесь, было бы неплохо соединить в одно сайт и приложение для смартфона. Если вы еще…
Повсеместное распространение смартфонов привело к огромному спросу на мобильные игры и приложения. Миллиарды пользователей гаджетов…
В перечне популярных чат-ботов с искусственным интеллектом Google Bard (Gemini) еще не пользуется такой популярностью…
Скрипт (англ. — сценарий), — это небольшая программа, как правило, для веб-интерфейса, выполняющая определенную задачу.…
Дедлайн (от англ. deadline — «крайний срок») — это конечная дата стачи проекта или задачи…