Что такое закрытие? Примеры использования в JavaScript и React


Что такое замыкание?

Если вы не совсем новичок в JavaScript и не знакомы с замыканиями, вы, вероятно, использовали замыкание, не зная об этом. Замыкание — это когда функция имеет доступ к переменным (может читать и изменять их), определенным в ее внешней области видимости, даже если функция выполняется вне области видимости, в которой она была определена. Закрытие — это функция, заключающая в себе ссылку (переменную) на свою внешнюю область видимости. Функции могут обращаться к переменным за пределами своей области видимости.

Вот простой пример, когда внешняя функция, возвращающая внутреннюю функцию, имеет доступ к переменной во внешней функции:

function outerFunction() {
  let outerFuncVar = "outside";
  function innerFunction() {
    console.log(`The value is: ${outerFuncVar}`);
  }
  return innerFunction();
}

outerFunction();
Войти в полноэкранный режим Выход из полноэкранного режима

Вывод консоли: Значение: outside.

 

Внешняя функция возвращает внутреннюю функцию, которая «закрывается» над переменной внешней функции outerFuncVar. Именно поэтому она называется закрывающей. Функция outerFunction, которая возвращает innerFunction, может быть вызвана в любом месте вне своей области видимости, и функция innerFunction будет иметь доступ к outerFuncVar, она может запомнить ее. Когда она будет вызвана, она сможет прочитать значение этой переменной.

Давайте изменим приведенный выше пример так, чтобы переменная outerFunction могла быть изменена и новое значение записывалось в журнал по истечении 5 секунд:

function outerFunction(input) {
  let outerFuncVar = input;
  function innerFunction() {
    setTimeout(() => {
      console.log(`The value is: ${input}`);
    }, 5000);
  }
  return innerFunction();
}

outerFunction("new value");
Вход в полноэкранный режим Выход из полноэкранного режима

Вывод консоли: The value is: new value

 

Даже после завершения выполнения outerFunction в приведенном выше примере, outerFuncVar все еще доступен через 5 секунд после вызова функции. JavaScript автоматически выделяет память при первоначальном объявлении переменных. После возвращения функции ее локальные переменные могут быть помечены для сбора мусора и удалены из памяти. Сборка мусора — это тип автоматического управления памятью, используемый JavaScript для освобождения памяти, когда выделенный блок памяти, такой как переменная и ее значение, больше не нужен.

Если бы outerFuncVar был собран в мусор сразу после вызова функции, это привело бы к ошибке, поскольку outerFuncVar больше не существовал бы. outerFuncVar не собирается в мусор, потому что JavaScript считает, что вложенная innerFunction все еще может быть вызвана, поскольку она используется в закрытии. JavaScript делает управление памятью за нас, в отличие от низкоуровневых языков, таких как C.

Вы также можете увидеть это сохранение ссылки закрытия на внешнюю переменную, вернув innerFunction из outerFunction и сохранив ее в переменной перед выполнением innerFunction:

function outerFunction() {
  let outerFuncVar = "outside";
  function innerFunction() {
    console.log(`The value is: ${outerFuncVar}`);
  }
  return innerFunction;
}

const innerFunct = outerFunction();
innerFunct();
Вход в полноэкранный режим Выход из полноэкранного режима

Вывод консоли: Значение: outside.

 

Если внешняя функция сама является вложенной функцией, такой как outerOuterFunction в приведенном ниже коде, то все закрытия будут иметь доступ ко всем своим областям внешней функции. В данном случае закрытие innerFunction имеет доступ к переменным outerFunction и outerOuterFunction:

function outerOuterFunction() {
  let outerOuterFuncVar = "outside outside";
  return function outerFunction() {
    let outerFuncVar = "outside";
    function innerFunction() {
      console.log(`The outerFunction value is: ${outerFuncVar}`);
      console.log(`The outerOuterFunction value is: ${outerOuterFuncVar}`);
    }
    return innerFunction;
  };
}

const outerFunct = outerOuterFunction();
const innerFunct = outerFunct();
innerFunct();
Вход в полноэкранный режим Выход из полноэкранного режима

Вывод консоли:

 

Можно также создать несколько экземпляров замыкания с независимыми переменными, над которыми они замыкаются. Рассмотрим контрпример:

function counter(step) {
  let count = 0;
  return function increaseCount() {
    count += step;
    return count;
  };
}

let add3 = counter(3); // returns increaseCount function. Sets step and count to 3
let add5 = counter(5); // returns increaseCount function. Sets step and count to 5

add3(); // 3
console.log(add3()); // 6

add5(); // 5
add5(); // 10
console.log(add5()); // 15
Войти в полноэкранный режим Выход из полноэкранного режима

 

Когда функция counter вызывается с помощью counter(3), создается экземпляр функции increaseCount, который имеет доступ к переменной count. step устанавливается в 3, это переменная параметра функции, а count устанавливается в 3 (count += step). Оно хранится в переменной add3. Когда функция counter вызывается снова, используя counter(5), создается новый экземпляр increaseCount, который имеет доступ к переменной count этого нового экземпляра. step устанавливается равным 5, а count устанавливается равным 5 (count += step). Оно хранится в переменной add5. Вызов этих различных экземпляров закрытия увеличивает значение count в каждом экземпляре на величину step. Переменные count в каждом экземпляре являются независимыми. Изменение значения переменной в одном замыкании не влияет на значения переменных в других замыканиях.

 

Более техническое определение замыкания

Закрытие — это когда функция помнит и имеет доступ к переменным в своей лексической / внешней области видимости, даже когда функция выполняется за пределами своей лексической области видимости. Замыкания создаются во время создания функции. Переменные организуются в единицы области видимости, такие как область видимости блока или области видимости функции. Области видимости могут вложены друг в друга. В данной области видимости доступны только переменные в текущей области видимости или в более высокой/внешней области видимости. Это называется лексической областью видимости. Лексический, согласно словарному определению, означает относящийся к словам или словарному запасу языка. В данном случае, вы можете думать, что это то, как происходит скопинг в языке JavaScript. Лексическая область видимости использует место объявления переменной в исходном коде для определения того, где переменная доступна в исходном коде. Область видимости определяется во время компиляции, а точнее, во время лексической обработки, компилятором JavaScript Engine, используемым для обработки и выполнения кода. Первый этап компиляции включает в себя лексирование / синтаксический анализ. Лексирование — это когда код преобразуется в лексемы, что является частью процесса преобразования кода в машиночитаемый код. О том, как работает движок JavaScript, вы можете прочитать в этой статье: JavaScript Visualized: JavaScript Engine.


 

Почему замыкания важны? Некоторые примеры

Вот несколько примеров использования замыканий в JavaScript и React.

 

JavaScript

Асинхронный код

Замыкания обычно используются в асинхронном коде, например: отправка POST-запроса с помощью Fetch API:

function getData(url) {
  fetch(url)
    .then((response) => response.json())
    .then((data) => console.log(`${data} from ${url}`));
}

getData("https://example.com/answer");
Вход в полноэкранный режим Выход из полноэкранного режима

Когда вызывается функция getData, она завершает выполнение до завершения запроса на выборку. Внутренняя функция fetch закрывает переменную параметра функции url. Это сохраняет переменную url.

 

Модули

Шаблон модуля JavaScript — это широко используемый шаблон проектирования в JavaScript для создания модулей. Модули полезны для повторного использования и организации кода. Шаблон модуля позволяет функциям инкапсулировать код, как это делает класс. Это означает, что функции могут иметь открытые и закрытые методы и переменные. Это позволяет контролировать, как различные части кодовой базы могут влиять друг на друга. Для этого необходимы замыкания для функциональных модулей. Функциональные модули представляют собой функциональные выражения с немедленным вызовом (IIFE). IIFE создает замыкание, которое имеет методы и переменные, доступ к которым возможен только внутри функции, они являются приватными. Чтобы сделать методы или переменные общедоступными, их можно вернуть из функции модуля. Замыкания полезны в модулях, поскольку они позволяют методам модуля быть связанными с данными в их лексическом окружении (внешней области видимости), переменными в модуле:

var myModule = (function () {
  var privateVar = 1;
  var publicVar = 12345;

  function privateMethod() {
    console.log(privateVar);
  }

  function publicMethod() {
    publicVar += 1;
    console.log(publicVar);
  }

  return {
    publicMethod: publicMethod,
    publicVar: publicVar,
    alterPrivateVarWithPublicMethod: function() {
      return privateVar += 2;
    },
  };
})();

console.log(myModule.publicVar); // 12345
console.log(myModule.alterPrivateVarWithPublicMethod()); // 3
myModule.publicMethod(); // 12346
console.log(myModule.alterPrivateVarWithPublicMethod()); // 5
console.log(myModule.privateVar); // undefined
myModule.privateMethod(); // Uncaught TypeError: myModule.privateMethod is not a function
Вход в полноэкранный режим Выход из полноэкранного режима

 

Функциональное программирование — керринг и композиция

Свертывание функции — это когда функция, принимающая несколько аргументов, написана таким образом, что она может принимать только один аргумент за раз. Она возвращает функцию, которая принимает следующий аргумент, которая возвращает функцию, принимающую следующий аргумент, … так продолжается до тех пор, пока все аргументы не будут предоставлены, после чего она возвращает значение. Это позволяет разбить большую функцию на более мелкие функции, каждая из которых выполняет определенные задачи. Это может облегчить тестирование функций. Вот пример функции curried, которая складывает три значения:

function curryFunction(a) {
  return (b) => {
    return (c) => {
      return a + b + c;
    };
  };
}
console.log(curryFunction(1)(2)(3)); // 6
Войти в полноэкранный режим Выйти из полноэкранного режима

 

Композиция — это когда функции объединяются для создания более крупных функций, это важная часть функционального программирования. Свернутые функции можно объединять в большие, сложные функции. Композиция может сделать код более читабельным благодаря описательным именам функций. Ниже приведен простой пример объединения и композиции, где есть две числовые функции (для простоты): five и six, которые используют функцию n, что позволяет вызывать их отдельно или компоновать с другими функциями, такими как функция plus. Функция isEqualTo проверяет, одинаковы ли два числа.

var n = function (digit) {
  return function (operator) {
    return operator ? operator(digit) : digit;
  };
};

var five = n(5);
var six = n(6);

function plus(prev) {
  console.log('prev = ', prev); // prev = 6
  return function (curr) {
    return prev + curr;
  };
}

function isEqualTo(comparator) {
  console.log('comparator = ', comparator); // comparator = 5
  return function (value) {
    return value === comparator;
  };
}

console.log(five()); // 5

// values calculated from the inside to the outside
// 1. six() => result1
// 2. plus(result1) => result2
// 3. five(result2) => final result
console.log(five(plus(six()))); // 11
console.log(isEqualTo(five())("5")); // false
Вход в полноэкранный режим Выход из полноэкранного режима

 

Подробнее о керринге и композиции вы можете прочитать в этой статье: Как использовать керринг и композицию в JavaScript.

 

Вот пример функции debounce с сайта https://www.joshwcomeau.com/snippets/javascript/debounce/, которая возвращает функцию и использует замыкание, как в примере со счетчиком, который мы использовали ранее:

const debounce = (callback, wait) => {
  let timeoutId = null;
  return (...args) => {
    window.clearTimeout(timeoutId);
    timeoutId = window.setTimeout(() => {
      callback.apply(null, args);
    }, wait);
  };
};
Войти в полноэкранный режим Выход из полноэкранного режима

 

Современные фронт-энд фреймворки/библиотеки, такие как React, используют модель композиции, в которой небольшие компоненты могут быть объединены для создания сложных компонентов.

 

React

Создание крючков

Вот функция, имитирующая хук useState. Начальное значение, геттер состояния, заключено в закрытие и действует как хранимое состояние:

function useState(initial) {
  let str = initial;
  return [
    // why is the state value a function? No re-render in vanilla JavaScript like in React.
    // if you just use the value (no function), then change it with the setter function(setState) and then the log value, it will reference a "stale" value (stale closure) -> the initial value not the changed value
    () => str,
    (value) => {
      str = value;
    },
  ];
}

const [state1, setState1] = useState("hello");
const [state2, setState2] = useState("Bob");
console.log(state1()); // hello
console.log(state2()); // Bob
setState1("goodbye");
console.log(state1()); // goodbye
console.log(state2()); // Bob
Войти в полноэкранный режим Выход из полноэкранного режима

Чтобы увидеть лучшую реализацию, в которой значение состояния не является функцией, ознакомьтесь со следующей статьей — Получение закрытия на React Hooks.

 

Замыкания запоминают значения переменных из предыдущих рендеров — это может помочь предотвратить ошибки асинхронности

В React, если у вас есть асинхронная функция, которая полагается на реквизиты, которые могут меняться во время выполнения асинхронной функции, вы можете легко получить ошибки при использовании компонентов класса из-за изменения значений реквизитов. Замыкания в функциональных компонентах React позволяют избежать подобных ошибок. Асинхронные функции, использующие значения реквизитов, используют замыкания для сохранения значений реквизитов на момент создания функции. При каждом рендеринге компонента создается новый объект props. Функции в компоненте создаются заново. Любые функции async, использующие переменные из props (или откуда-либо еще), запоминают переменные благодаря закрытию. Если компонент, в котором находится асинхронная функция, перерендерится и реквизиты изменятся (новые значения) во время вызова асинхронной функции, вызов асинхронной функции будет по-прежнему ссылаться на реквизиты из предыдущего рендера, где была определена функция, поскольку значения были сохранены благодаря закрытию. Пример этого вы можете увидеть в статье — Как React использует замыкания, чтобы избежать ошибок.


 

Заключение

Мы узнали, что такое замыкания на некоторых примерах и рассмотрели несколько примеров использования в JavaScript и React. Чтобы узнать больше о замыканиях, вы можете ознакомиться со статьями, ссылки на которые приведены ниже.


 

Ссылки / дополнительное чтение

  • Статья MDN Closures
  • Книга You Don’t Know JS — Getting Started — Chapter 3
  • Книга You Don’t Know JS — Getting Started — Appendix B
  • Статья Dan Abramov Closure
  • Основы шаблона модулей в JavaScript
  • Паттерн проектирования модулей в JavaScript
  • Как использовать Currying и Composition в React
  • Получение закрытия на крючках React
  • Как React использует замыкания для предотвращения ошибок

Оцените статью
Procodings.ru
Добавить комментарий