На примерах давайте познакомимся со свойствами замыканий и интересными вариантами их использования.
Первоначально опубликовано в blog.shams-nahid.com.
Замыкания позволяют методу обращаться к переменным другого метода, даже если этот метод покинул стек. Замыкания представляют собой комбинацию function
и lexical scope
.
Чтобы увидеть замыкания в действиях,
// A simple closures example function hoc() { // "willBePreserved" is a closure const willBePreserved = "Screaming Eagle Cabernet Sauvignon"; return function child() { console.log(willBePreserved); }; }
const returnedMethod = hoc(); returnedMethod();
Здесь метод hoc
имеет свойство willBePreserved
. Этот willBePreserved
принадлежит hoc
и также используется в методе child
. Когда мы закончили вызывать метод hoc
, пришел сборщик мусора и удалил hoc
из стека вызовов. Но, поскольку метод child
использует willBePreserved
, он будет сохранен. Это willBePreserved
закрытие.
Характеристики
Пара свойств замыканий:
- Замыкания могут использоваться с циклом событий
- Замыкания не сохранят переменную, на которую нет ссылки
- Замыкания игнорируют правила подъема
Замыкания могут использоваться с циклом событий
Давайте посмотрим на замыкания в действии с циклом событий,
// An example of using Closures with WEB API
function closuresWithEventLoop () {
const preservedVal = 'I am preserved!';
setTimeout(() => {
console.log(preservedVal); // display "I am preserved!"
}, 5000);
}
Здесь setTimeout
отправится в веб-API для выполнения. Тем временем сборщик мусора удалит метод closuresWithEventLoop
, но сохранит метод preservedVal
. Через 5 секунд в консоли будет напечатано preservedVal
.
Замыкания не сохранят переменную, на которую нет ссылки
Замыкания сохраняют только переменную из сборщика мусора, на которую ссылаются.
// An example of using closures with WEB API function closuresWithLocalVariable () { /** * Since not referenced in the returned method, * the "notPreservedVal" will be removed from garbage collector */ const notPreservedVal = 'I am not preserved!';
/** * Will be preserved from the garbage collector, * as it is used in the returned method */ const preservedVal = 'I am preserved!';
return function() { console.log(preservedVal); // display "I am preserved!" } }
const returnedMethod = closuresWithLocalVariable(); returnedMethod(); // display "I am preserved!"
В этом примере, поскольку notPreservedVal
нигде не упоминается, он не будет сохранен.
Замыкания игнорируют правило подъема
JavaScript не поднимает const
или let
. Поднятие доступно только при объявлении переменной с ключевым словом var
. В следующем примере мы видим, что даже если переменная объявлена с const
, она будет отображаться и печататься.
// Example of closures ignores hoisting function closuresHoising() { setTimeout(function() { console.log(willBePreserved); }, 1000); // "const" never hoisted, but here we can print it in "setTimeout" method const willBePreserved = "I am hoisted even declared with 'const'"; }
closuresHoising(); // Display "I am hoisted even declared with 'const'"
Интересный пример использования замыканий
С замыканиями мы можем придумать пару интересных вариантов использования кода,
- Оптимизация памяти
- Инкапсуляция памяти
- Интересный узор
Оптимизация памяти
Рассмотрим метод, возвращающий историю страны. Для этого он сначала загружает всю страну, их историю и сохраняет их в хэш-карте. Затем по названию страны возвращает историю конкретной страны.
// Use to fetch country list and their history // Then return the history of a country passed as param function getCountryHistory (countryName) { // Fetch all the country list from a public api // Sort them locally // Fetch history for each country // Load country history in a hashMap { [key: countryName]: countryHistory const countryHistory = { country_name_1: 'country_1_history', country_name_2: 'country_2_history', country_name_3: 'country_3_history', };
return countryHistory[countryName]; }
getCountryHistory('country_name_1'); // Display 'country_1_history'
Вы могли заметить, что каждый раз, когда мы вызываем метод getCountryHistory
, он извлекает все данные, сохраняет их в памяти и возвращает результат. С замыканиями мы можем оптимизировать решение, поэтому данные будут получены только один раз. Каждый раз, когда мы вызываем метод, результат будет показан из замыканий. Давайте посмотрим оптимизированное решение в действии,
// optimize solution // Use to fetch country list and their history // Store them in a hashMap as closures function loadCountryHistory () { // Fetch all the country list from a public api // Sort them locally // Fetch history for each country // Load country history in a hashMap { [key: countryName]: countryHistory const countryHistory = { country_name_1: 'country_1_history', country_name_2: 'country_2_history', country_name_3: 'country_3_history', };
return function(countryName) { console.log(countryHistory[countryName]); } }
// this loads the country history in closures const getCountryHistory = loadCountryHistory();
getCountryHistory('country_name_1'); // Display 'country_1_history' getCountryHistory('country_name_2'); // Display 'country_2_history'
Интересный узор
Давайте рассмотрим функцию initialize()
, и она устанавливает некоторое глобальное значение. Важно то, что это должно быть вызвано только один раз.
let globalKey;
function initialize() { globalKey = "value"; console.log("Global key is set"); }
initialize(); initialize(); initialize();
Здесь функция вызывается 3 раза. С закрытием нам нужно вызвать его один раз.
let globalKey;
function initialize() { let isCalled = false; return () => { if (isCalled) { return; } isCalled = true; globalKey = "value"; console.log("Global key is set"); }; }
const startOnce = initialize(); startOnce(); startOnce(); startOnce();
Теперь это будет вызываться только один раз.
Другим подходом к решению проблемы может быть обновление ссылки на функцию после инициализации в первый раз.
let globalKey;
function initialize() { globalKey = "value"; console.log("Global key is set"); initialize = () => { console.log("Aboarting! already set the value"); }; }
initialize(); initialize();
Используя IIFE, мы можем сделать некоторые улучшения,
let globalKey;
const initialize = (() => { let called = 0; return () => { if (called) { console.log("Already Set"); return; } called = 1; globalKey = "Global Value"; console.log("Set the value"); }; })();
initialize(); initialize(); initialize();
Мы увидим результат
Set the value
Already Set
Already Set
Еще один интересный узор [Бонус]
Рассмотрим следующий фрагмент,
const array = [1, 2, 3, 4];
function traverse() { for (var i = 0; i < array.length; i++) { setTimeout(function () { console.log(i); }, 1000); } }
traverse();
Выход будет,
4 4 4 4
В этом случае переменная использовала подъем и получила последний индекс. использование let
вместо var
может решить проблему.
const array = [1, 2, 3, 4];
function traverse() { for (let i = 0; i < array.length; i++) { setTimeout(function () { console.log(i); }, 1000); } }
traverse();
Поскольку мы находимся в мире замыканий, давайте решим это с помощью IIFE и замыканий.
const array = [1, 2, 3, 4];
function traverse() { for (var i = 0; i < array.length; i++) { (function (val) { setTimeout(function () { console.log(val); }, 1000); })(i); } }
traverse();
Наш желаемый результат сейчас,
0 1 2 3
Последние мысли
Замыкания — очень интересные концепции и мощные инструменты. Пожалуйста, дайте мне знать, если вы думаете об использовании замыканий в ежедневном программировании.
Список литературы: JavaScript: The Advanced Concepts автора Andrei Neagoie