На примерах давайте познакомимся со свойствами замыканий и интересными вариантами их использования.

Первоначально опубликовано в 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