В этой статье мы поговорим о замыканиях в 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.
Давайте сперва разберем, зачем вообще нужна такая утилита:
- Мы не хотим постоянно определять базовый URL (или другие общие параметры) для каждого вызова fetch. Поэтому мы создадим механизм, который будет хранить базовый URL/параметры как состояние.
- Для удаления лишнего кода.
- Для обеспечения модульности в кодовой базе.
Теперь углубимся в детали этой утилиты.
Наша утилита 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».
[customscript]techrocks_custom_after_post_html[/customscript]
[customscript]techrocks_custom_script[/customscript]