Введение в замыкания, защиту данных и частичное применение функций
Я познакомился с замыканиями во время изучения Ruby в рамках учебной программы Launch School. Замыкания — это основная концепция программирования, и они появляются во многих разных языках. У них есть ряд вариантов использования, включая добавление гибкости к коду и контроль доступа к конфиденциальным данным. Понимание замыканий и возможность их реализации на любом языке, который вы используете, является важным навыком для разработчика. В этой статье мы рассмотрим, как замыкания работают в JavaScript, и рассмотрим некоторые распространенные варианты использования. В этой статье вы увидите, что замыкания являются ключевыми понятиями для реализации функций обратного вызова и защиты данных. Мы также рассмотрим использование замыканий для частичного применения функций.
Что такое замыкание?
В широком смысле замыкание — это фрагмент кода, инкапсулированный вместе с элементами окружающего его контекста. Затем этот код можно передать в программе и выполнить в другом месте. При выполнении код в замыкании может получить доступ к данным из своей исходной среды, даже если эти данные недоступны в новом контексте выполнения.
То, как именно реализуются замыкания, отличается от языка к языку. В то время как Ruby использует блоки и специальный объект, называемый «proc», для создания замыканий, JavaScript встраивает концепцию замыкания в поведение своих функций. В частности, замыкание в JavaScript — это определение функции в сочетании со всеми переменными области видимости, которые она использует. Продемонстрируем это на простом примере:
В строках 1–3 мы объявляем три переменные — city
, forecast
и news
— и инициализируем их строковыми значениями. Поскольку они объявлены в области верхнего уровня, на них можно ссылаться в любом месте программы; они имеют глобальный охват. В строках 5–7 мы объявляем функцию reportWeather
, которая выводит строку на консоль. Выражение, переданное в console.log()
, представляет собой литерал шаблона, который интерполирует значения city
и forecast
в строку. Где в этом коде концепция замыкания?
Когда reportWeather
объявляется, он распознает две вещи: (1) ему нужны city
и forecast
для выполнения, и (2) обе эти переменные существуют в текущей области. На основе этих наблюдений reportWeather
формирует замыкание, включающее имена этих двух переменных. В строке 9 вызывается reportWeather()
. Когда строка 6 выполняется, reportWeather
просматривает свою локальную область видимости (внутри тела функции), чтобы увидеть, объявлены ли там какие-либо переменные с именами city
и forecast
. Поскольку он их не находит, он затем проверяет имена переменных в своем закрытии. city
и forecast
существуют в замыкании, поэтому reportWeather
может использовать эти имена переменных для поиска текущего значения этих переменных.
Обратите внимание, что хотя переменная news
также находится в области видимости при определении функции, она не включается в замыкание reportWeather
. Функции формируют замыкания только с переменными, которые им действительно нужны для запуска.
Замыкания образуются лексически
Другая характеристика замыканий в JavaScript заключается в том, что они формируются в соответствии со структурой (или компоновкой) программы. Замыкание не создается во время выполнения. Эту черту часто называют лексической чертой. Проверьте это — обратите особое внимание на значение news
и вывод reportNews()
:
Лексическая природа замыканий является причиной того, что reportNews()
выводит "BREAKING! Blazers win!"
вместо "BREAKING! Thorns win!"
. Когда функция reportNews
объявлена в строках 3–5, if формирует замыкание с переменной news
, объявленной в строке 1. Замыкание ссылается, в частности, на эту переменную. Эта переменная верхнего уровня news
скрыта внутри блока в строках 7–11 другой переменной с тем же именем. Эта переменная news
с локальной областью видимости является переменной, которая находится в области видимости при вызове reportNews()
. Однако, поскольку reportNews
имеет замыкание с переменной news
из строки 1, при выполнении он ищет значение этой переменной. Это демонстрирует, что замыкание было сформировано в соответствии с тем, где было определено reportNews
, а не там, где оно в конечном итоге было вызвано.
Замыкания содержат ссылки
Когда замыкание сформировано, оно не захватывает значения переменных, которые являются его частью. Замыкание содержит только ссылки на переменные. Это имеет важные последствия. Хотя замыкание формируется лексически, значение каждой переменной в замыкании извлекается во время выполнения функции. Это часть того, что делает замыкания такими мощными — значения переменных в замыкании могут изменяться во время выполнения:
Когда reportNews
определено в строках 3–5, оно образует замыкание с переменной news
. Замыкание содержит идентификатор news
, но не значение 'Blazers win'
. В строке 7 news
переназначается новой строке 'Thorns win'
. Когда reportNews()
вызывается в строке 9, она ищет текущее значение news
, равное 'Thorns win'
, и интерполирует его в литерал шаблона.
Тот факт, что замыкания в JavaScript имеют как лексический характер, так и характер времени выполнения, может привести к путанице. Чтобы не запутаться, обратите внимание на то, какие переменные находятся в области видимости, где определена функция. После этого обратите внимание на любые изменения, которые происходят с теми переменными до вызова функции.
Функции обратного вызова
Замки — отличное решение множества проблем. Наиболее распространенные варианты использования включают функции обратного вызова, инкапсуляцию данных и частичное применение функций.
Функции обратного вызова, вероятно, являются наиболее распространенными местами, где разработчик сталкивается с замыканиями. Функция обратного вызова формирует замыкание с переменными в окружающем ее контексте и делает эти переменные доступными для метода, который иначе не имел бы к ним доступа. Вот краткий пример:
В строках 1–2 мы объявляем переменные numbers
и increment
и инициализируем их массивом и числом соответственно. В строке 4 объявлена переменная biggerNums
с инициализатором. В инициализаторе map()
вызывается для массива, на который ссылается numbers
, и анонимная стрелочная функция передается map()
в качестве аргумента функции обратного вызова. Поскольку increment
не находится в области действия, где метод map()
определен (он находится в области действия, где вызывается map()
, но не там, где он реализован), map()
естественно не имеет доступа к increment
. Однако функция обратного вызова имеет increment
как часть закрытия, поэтому она может искать значение increment
каждый раз, когда map()
вызывает функцию обратного вызова. Это позволяет map()
использовать значение increment
, даже если map()
не имеет к нему прямого доступа.
Защита данных
Замыкания — это мощный инструмент для реализации защиты данных. Как правило, программа, которая обрабатывает конфиденциальную информацию, должна препятствовать легкому доступу к ней или ее изменению. Вместо этого должно быть только несколько четко определенных путей для чтения или обновления данных. Написание функции, которая возвращает замыкание, является одним из способов создания этой функциональности.
Чтобы проиллюстрировать это, давайте напишем небольшую программу, которая обрабатывает кредитный рейтинг. В общем, есть два способа проверить чью-то кредитную историю. Во-первых, это программный запрос, который обычно используют потребительские сайты, такие как Credit Karma. Мягкий запрос может найти кредитный рейтинг, не влияя на его значение. Однако, когда вы идете подать заявку на кредит, кредитор выполнит жесткий запрос, который может повлиять на кредитный рейтинг на небольшую сумму. Для нашей программы это единственные два способа получить доступ к кредитному рейтингу. Мы не хотим, чтобы кто-либо имел доступ к данным без использования одного из двух каналов, которые мы определяем.
Давайте определим функцию с именем createCheckers
, которая генерирует кредитный рейтинг и возвращает две функции, которые могут получить доступ к этому значению:
В строках 4–6 и 8–11 мы объявляем две константы — softInquiry
и hardInquiry
— и инициализируем их анонимными функциональными выражениями. Обе эти функции образуют замыкания, которые включают ссылку на переменную creditScore
, объявленную в строке 2. Когда createCheckers()
вызывается в строке 16, она возвращает эти функции в виде массива из двух элементов, и мы используем деструктурирование массива, чтобы присвоить каждую из них новому элементу. объявленная переменная, softInquiry
и hardInquiry
. Функции, представленные softInquiry
и hardInquiry
, теперь являются единственными способами доступа к значению creditScore
— его область действия ограничена телом функции createCheckers
.
softInquiry()
вызывается в строке 18, и, поскольку она имеет замыкание с creditScore
, она может искать свое значение и возвращать его в строке 5. Затем мы вызываем hardInquiry()
в строке 19. Эта функция уменьшает creditScore
на 5
в строке 9, прежде чем вернуть свое значение. значение в строке 10. Когда мы выполняем еще один мягкий запрос в строке 20, мы видим, что кредитный рейтинг действительно был поврежден, и после жесткого запроса он остается 595
. Используя замыкания, мы смогли создать систему, в которой к значению можно получить доступ только двумя способами: его можно было либо прочитать без ущерба, либо прочитать за 5 баллов. Никакие другие манипуляции с кредитным рейтингом невозможны.
Применение частичных функций
На мой взгляд, это один из самых сложных способов использования замыканий в JavaScript. Мы рассмотрим один простой пример и оставим более глубокое погружение на другой день. Допустим, у нас есть функция, которая выполняет какую-то задачу. Единственная проблема заключается в том, что для правильной работы функции требуется определенное минимальное количество аргументов, а API, который мы используем для вызова функции, не может передать достаточное количество аргументов. Мы можем использовать замыкание, чтобы «применить» некоторые аргументы к нашей функции до того, как она будет фактически вызвана в программе. Взглянем:
В строках 1–3 у нас есть функция joinWords
, которая принимает два строковых аргумента и возвращает результат их объединения. Это прекрасно работает, пока мы не столкнемся с ситуацией, когда нам нужна функциональность joinWords
, но мы можем передать только один аргумент. Это происходит в строках 12 и 13, где мы вызываем функцию с именем greet
, которая принимает только один аргумент — имя — но должна вывести строку, которая также содержит приветствие.
Мы достигаем этого, определяя функцию createGreeter
, которая принимает приветствие в качестве аргумента и возвращает анонимную функцию, которая использует это значение greeting
для вызова joinWords
. Когда эта анонимная функция создается, она формирует замыкание с переменной greeting
и «фиксирует» ссылку на эту переменную в списке аргументов joinWords
. Когда createGreeter
возвращается в строке 11, он возвращает определение функции, которое может принимать один аргумент. Когда эта функция, которая теперь назначена greet
, вызывается, она принимает один аргумент, используя его и переменную greeting
из своего замыкания для вызова joinWords
с двумя аргументами. Мы успешно создали способ получить функциональность joinWords
с помощью вызова функции с меньшим количеством аргументов, чем требуется joinWords
. В этом суть применения частичных функций.
Надеюсь, теперь полезность замыканий ясна. Они являются основным компонентом JavaScript, и на то есть веские причины. В процессе использования языка вы почти наверняка будете их использовать, даже если не осознаете этого в тот момент. Возможность распознавать замыкания значительно упростит понимание и отладку кода. Возможность реализовать их для решения проблемы добавляет незаменимый инструмент в ваш пояс инструментов. Практика замыканий до тех пор, пока они не станут интуитивно понятными, стоит потраченного времени и усилий.
Это последняя из трех статей, которые я написал по фундаментальным темам JavaScript. Не стесняйтесь ознакомиться с первыми двумя статьями JavaScript Primer из этой серии: переменные и область видимости и подъем.