Доброго времени суток, гики веб-разработки. Сегодня мы углубим ваши знания языка и разберем замыкания в JavaScript. Это очень важный, ключевой раздел при изучении JS, без которого по сути и «каши не сваришь».
Поэтому в текущей публикации мы пройдемся с вами по основным моментам замыкания для того, чтобы не быть чайниками. Я объясню, что это такое и с какими ошибками можно столкнуться. Также приведу несколько примеров для лучшего понимания материала. Как говорится: «Меньше слов, больше дела». Так что за дело!
Что подразумевает под собой замыкание
В JavaScript замыкание – это обычные вложенные функции, которые используют свободные (независимые) переменные, находящиеся в их окружении.
Такое окружение называется лексической областью видимости – Lexical scoping. Если более простыми словами, то это механизм, который позволяет вложенным функциям использовать переменные, объявленные вовне их тела, и «замыкать» последние на себе.
Рассмотрим пример. В коде создается функция с названием IntCounter (), в которой объявляется локальная переменная calls и вложенная функция. Последняя должна возвращать количество вызовов в основном коде.
1 2 3 4 5 6 7 8 9 10 | function IntCounter() { var calls = 0; return function() { return ++calls; } } var CountСalls = IntCounter (); CountСalls(); //1 CountСalls(); //2 CountСalls(); //3 |
В этом случае вложенной функции доступна локальная переменная внешней. И в добавок к этому данная переменная продолжает существовать и быть доступной для вложенной, несмотря на завершение выполнения основной функции.
Именно поэтому в прикрепленном выше примере переменная calls продолжает свое существование и сохраняет последнее присвоенное значение.
Почему данный механизм возможен?
Вот тут будьте внимательны!!! Попытайтесь хорошенько разобрать и запомнить прочитанное. В будущем это поможет вам понимать более сложные вещи в JS.
Итак, ссылки на внешние переменные, объекты хранятся во внутреннем свойстве вложенной функции под названием [[Scope]]. Это скрытое свойство, которое присваивается функциям при их создании и ссылается на их Lexical scoping.
[[Scope]] привязывается к конкретной функции и таким образом создает связь между ней и ее местом рождения. Значение [[Scope]] сохраняется и поэтому в примерах выше была возможность получить последнее значение и увеличить его.
Практическое применение замыканий
Данная штука очень полезна на практике. Особенно часто к ней обращаются в веб-приложениях, в которых используется множество событий.
Так как замыкания позволяют простым способом связать некоторые действия, переменные, параметры и сохранять значения после отработки методов, то они значительно упрощают написание определенных задач во время их программной реализации.
Для примера можно привести тривиальную задачу, которая иногда встречается при программировании онлайн-библиотек, блогов со статьями или других сайтов, где можно читать какую-либо литературу. Ваш сервис предлагает пользователю прочтение каких-то книг.
После открытия определенного источника для юзера стандартный шрифт может оказаться слишком мелким или наоборот слишком крупным. Поэтому ему предлагается выбрать размер символов.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | <!DOCTYPE html> <html> <head> <title>Практический пример использования замыкания</title> <meta charset="utf-8"> <style> body { font-family: Arial, sans-serif; font-size: 14px; } h1 { font-size: 1.5em; } h2 { font-size: 1.2em; } </style> </head> <body> <h2>Выберите размер шрифта</h2> <br/> <button id="size_12">12</button> <button id="size_16">16</button> <button id="size_20">20</button> <br/><br/> <p>Здесь написан какой-то текст исторического романа.</p> <h1>Том первый. Глава вторая.</h1> <p>Продолжение увлекательной истории...</p> <script> function ChangeSize(newSize) { return function() { document.body.style.fontSize = newSize + 'px'; }; } var size12 = ChangeSize(12); var size16 = ChangeSize(16); var size20 = ChangeSize(20); document.getElementById('size_12').onclick = size12; document.getElementById('size_16').onclick = size16; document.getElementById('size_20').onclick = size20; </script> </body> </html> |
Наиболее распространенные ошибки
К частым ошибкам можно отнести замыкание в цикле. Для лучшего понимания представьте, что у вас имеется программа, в которую вы вносите изучаемые вами английские слова. Для проверки правильности перевода, подсказка выводится при наведении на конкретное слово.
Обычно новички пишут код следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | <!DOCTYPE html> <html> <head> <title>Помощник при изучении английских слов</title> <meta charset="utf-8"> </head> <body> <p id="help">Наведите на слово для получения перевода.</p> <p id="1">elections</p> <p id="2">electricity</p> <p id="3">electric</p> <script> function showTranslation (translation) { document.getElementById('help').innerHTML = translation; } function DictionaryHelp() { var helpText = [ {'id': '1', 'help': 'Выборы'}, {'id': '2', 'help': 'Электричество'}, {'id': '3', 'help': 'Электрический'} ]; for (var i = 0; i < helpText.length; i++) { var item = helpText[i]; document.getElementById(item.id).onmouseover = function() { showTranslation (item.help); } } } DictionaryHelp(); </script> </body> </html> |
Однако при запуске программы и наведении на любое из слов, ответ всегда будет один и тот же – перевод слова «электрический».
Почему же так? На самом деле все функции, выполняющиеся как обработчики событий, реализуют все то же замыкание. Поэтому в коде было создано целых три замыкания (так как элементов в цикле всего три).
Однако все они ссылаются на одно и то же окружение, ведь ко времени выполнения события цикл уже был пройден и конечным обработанным элементом остался последний в массиве.
Для решения этой проблемы в новых версиях (начиная с ECMAScript 6) можно использовать ключевое let. В других ситуациях следует обратиться за помощью к function factory.
Для реализации такого подхода стоит написать дополнительную функцию, которая позволит создать три отдельные лексические области видимости.
Скрипт изменится следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | function showTranslation(translation) { document.getElementById('help').innerHTML = translation; } function HelpCallback(help) { return function() { showTranslation(help); }; } function DictionaryHelp() { var helpText = [ {'id': '1', 'help': 'Выборы'}, {'id': '2', 'help': 'Электричество'}, {'id': '3', 'help': 'Электрический'} ]; for (var i = 0; i < helpText.length; i++) { var item = helpText[i]; document.getElementById(item.id).onmouseover = HelpCallback(item.help); } } DictionaryHelp(); |
Если вам понравилась статья, то подписывайтесь на обновления блога и обязательно рассказывайте о нем единомышленникам. Пока-пока!
С уважением, Роман Чуешов
Роман, объясните пожалуйста мне что вызывает функцию showTranslation в последнем примере? Ведь после срабатывания обработчика событий в 18й строчке вызывается функция HelpCallback, которая только возвращает, но не вызывает функцию showTranslation