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

0
719
views
javascript logo

Хочешь проверить свои знания по JS?

Подпишись на наш канал с тестами по JS в Telegram!

×

В этой статье мы поговорим о замыканиях в JavaScript. Я познакомлю вас с определением замыкания, приведу простой пример, а также расскажу о некоторых преимуществах и недостатках использования замыканий.

Чтобы понять эту статью, вы должны разбираться в том, как работает контекст выполнения JavaScript, а также знать, что такое Fetch API и как его использовать.

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

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

Чтобы это понять, давайте разберемся, что такое цепочка областей видимости.

Суть цепочки областей видимости в том, что родительская область видимости не имеет доступа к переменным внутри своей дочерней области, но дочерняя имеет доступ к переменным, присутствующим в ее родительских областях.

Давайте разберем это на примере:

let buttonProps = (borderRadius) => {
	const createVariantButtonProps = (variant, color) => {
		const newProps = {
			borderRadius,
			variant,
			color
		};
		return newProps;
	}
	return createVariantButtonProps;
}

Как видите, у нас есть функция buttonProps. Она принимает в качестве аргумента borderRadius. Мы будем считать функцию buttonProps нашей родительской функцией.

У нас есть еще одна функция, определенная внутри родительской, — createVariantButtonProps. Эта функция принимает variant и color в качестве аргументов. Возвращает она объект, содержащий переменную borderRadius, которая присутствует вне области видимости createVariantButtonProps.

Но возникает вопрос: как внутренняя функция разрешает переменные, присутствующие в родительской области?

Благодаря лексической области видимости синтаксический анализатор JS знает, как разрешать переменные, присутствующие в его текущей области, т.е. фактически знает, как разрешать переменные, присутствующие во вложенных функциях.

Поэтому createVariantButtonProps будет иметь доступ к переменным, представленным во внешней функции buttonProps.

В приведенном выше примере внутренняя функция createVariantButtonProps является замыканием. Чтобы понять суть замыканий, мы сначала рассмотрим их характеристики:

  • Даже если внешняя функция перестает существовать, замыкание по-прежнему имеет доступ к ее переменным.
  • Замыкания не имеют доступа к параметру args своей внешней функции.

Давайте разберем подробнее первый пункт.

Даже если внешняя функция перестает существовать, внутренняя все равно имеет доступ к родительским переменным

Это основная функциональность любого замыкания. В этом сама суть замыканий и принцип работы.

Чтобы увидеть это в действии, мы выполним приведенную выше функцию buttonProps:

let primaryButton = buttonProps("1rem"); 

Вызов функции buttonProps вернет нам другую функцию, которая является нашим замыканием.

Теперь давайте выполним это замыкание:

const primaryButtonProps = primaryButton("primary", "red");

В результате выполнения замыкание возвращает следующий объект:

{
   "borderRadius":"1rem",
   "variant":"primary",
   "color":"red"
}

Здесь снова возникает вопрос: как функция primaryButton может иметь доступ к переменной borderRadius, которой не было внутри нее?

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

Но почему замыкания по-прежнему имеют доступ к переменным, которые определены вне их области видимости, даже если внешняя функция перестает существовать (например, borderRadius)?

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

Вариант использования замыкания: создание утилиты fetch

Теперь, когда мы узнали, что такое замыкания, мы создадим отличную служебную функцию общего назначения. Она будет обрабатывать различные методы запросов, такие как GET и POST, при помощи REST API.

Для этого юзкейса мы будем использовать:

  • API JSON placeholder. Так мы получим некоторые поддельные данные, которые сможем редактировать с помощью REST API
  • fetch API JavaScript.

Давайте сперва разберем, зачем вообще нужна такая утилита:

  1. Мы не хотим постоянно определять базовый URL (или другие общие параметры) для каждого вызова fetch. Поэтому мы создадим механизм, который будет хранить базовый URL/параметры как состояние.
  2. Для удаления лишнего кода.
  3. Для обеспечения модульности в кодовой базе.

Теперь углубимся в детали этой утилиты.

Наша утилита fetch будет выглядеть следующим образом:

const fetchUtility = (baseURL, headers) => {
  const createFetchInstance = (route, requestMethod, data) => {
    const tempReq = new Request(`${baseURL}${route}`, {
      method: requestMethod,
      headers,
      data: data || null
    });
    return [fetch, tempReq];
  };

  return createFetchInstance;
};
  • fetchUtility принимает два параметра: baseURL и headers. Они будут использоваться позже в замыкании для создания базового URL-адреса и заголовков.
  • Затем у нас есть createFetchInstance, которая принимает в качестве параметров route, requestMethod и data.
  • Далее эта функция создает новый объект запроса, который создаст наш URL-адрес, используя код ${baseURL}${route}. Мы также передаем объект, который состоит из типа метода запроса, заголовков и данных, если они доступны.
  • Затем мы возвращаем экземпляр fetch API вместе с объектом запроса.
  • Наконец, мы возвращаем функцию createFetchInstance.

Теперь давайте посмотрим на эту функцию в действии. Вызовем нашу функцию fetchUtility для инициализации baseURL:

const fetchInstance = fetchUtility("https://jsonplaceholder.typicode.com");
  • fetchInstance теперь имеет значение замыкания функции fetchUtility.
  • Далее мы передаем маршрут и тип запроса замыканию fetchInstance:
const [getFunc, getReq] = fetchInstance("/todos/1", "GET");

Как видите, это возвращает нам массив экземпляров fetch API и тело запроса, которое мы настроили.

Наконец, мы можем использовать getFunc fetch API для вызова запроса getReq:

getFunc(getReq)
  .then((resp) => resp.json())
  .then((data) => console.log(data));

Мы также можем создать запрос POST, аналогичный приведенному выше запросу GET. Нам просто нужно снова вызвать fetchInstance:

const [postFunc, postReq] = fetchInstance(
  "/posts",
  "POST",
  JSON.stringify({
    title: "foo",
    body: "bar",
    userId: 1
  })
);

Для выполнения запроса POST мы можем сделать то же, что сделали для запроса GET:

postFunc(postReq)
  .then((resp) => resp.json())
  .then((data) => console.log(data));

Внимательно посмотрев на этот пример, мы увидим, что внутренняя функция createFetchInstance имеет доступ к переменным, присутствующим в ее цепочке областей видимости. Имена переменных разрешаются с помощью лексической области видимости во время определения createFetchInstance.

Таким образом, замыкание ссылается на переменные baseURL и headers во время своего определения даже после того, как внешняя функция fetchUtility перестала существовать.

Если мы подумаем о замыканиях с другой точки зрения, то они помогают нам поддерживать состояния, такие как baseURL и headers, которые мы можем использовать при вызовах функций.

Преимущества замыканий

Вот некоторые преимущества замыканий:

  • Они позволяют прикреплять переменные к контексту выполнения
  • Переменные в замыканиях могут помочь сохранить состояние, которое вы сможете использовать позже
  • Замыкания обеспечивают инкапсуляцию данных
  • Они помогают удалить лишний код
  • Они помогают поддерживать модульный код

Недостатки замыканий

Есть два основных недостатка злоупотребления замыканиями:

  • Переменные, объявленные внутри замыкания, не удаляются сборщиком мусора
  • Слишком много замыканий может замедлить работу вашего приложения. Это связано с дублированием кода в памяти.

Итоги

Итак, замыкания могут быть очень полезны, если вы хотите использовать или реализовывать некоторые шаблоны проектирования. Они также помогают писать аккуратный и модульный код.

Перевод статьи «Closure in JavaScript – Explained with Examples».

ОСТАВЬТЕ ОТВЕТ

Please enter your comment!
Please enter your name here