Замыкание – это функция, связанная со своим лексическим окружением
JavaScript — это язык с лексической областью видимости. Это означает, что функции используют область действия переменной, которая была в силе, когда они были определены (не область действия переменной, действовавшая при их вызове).
Технически все функции JavaScript являются замыканиями, но поскольку большинство функций вызываются из той же области видимости, в которой они были определены, то наличие замыкания не имеет значения.
Замыкания обычно используются для инкапсуляции (возможность иметь частные свойства для объектов), функционального программирования (каррированные функции, частичные приложения) и для предоставления доступа к переменным внутри прослушивателей событий.
Давайте рассмотрим каждый из этих вариантов использования, чтобы помочь нам понять, что такое закрытие.
Инкапсуляция
Скажем, у нас есть фабричная функция, которая возвращает объект-счетчик:
const counter = () => ({ n: 0, count() { this.n++ }, reset() { this.n = 0 } })
const counter1 = counter(); counter1.count(); counter1.count(); console.log(counter1.n) // 2 counter1.n = 0; // << We don't want this console.log(counter1) // { n: 0, ... } uh oh!
Ошибочный или вредоносный код может сбросить счетчик без вызова метода reset()
, как показано выше.
Как упоминалось в моем посте об инкапсуляции, это нарушает фундаментальный принцип хорошего дизайна программного обеспечения:
"Программируйте интерфейс, а не реализацию". — Банда четырех, «Шаблоны проектирования: элементы многоразового объектно-ориентированного программного обеспечения»
Мы хотим иметь возможность общаться только с counter
, используя его интерфейс и передавая сообщения (методы), такие как count()
или reset()
. Мы не хотим иметь возможность напрямую обращаться к таким свойствам, как n
. К сожалению, свойство n
является частью общедоступного интерфейса для этого объекта, поэтому им легко манипулировать. Давайте изменим это. Закрытие может помочь нам здесь. Взгляните на этот пересмотренный пример:
const counter = () => { let n = 0; return { count() { n++ }, reset() { n = 0 }, getCount() { console.log(n) } } }
const counter1 = counter(); counter1.count(); counter1.count(); counter1.getCount() // 2 console.log(counter1.n) // undefined
Прежде чем мы разберем это. Пересмотрите наше определение замыкания — функции, связанной со своим лексическим окружением. Лексическое окружение представляет собой область действия переменной, которая действовала при определении функции.
n
находится в области видимости, когда определены count
, reset
и getCount
, поэтому, когда счетчик возвращается и создается объект, единственный код, который будет иметь прямой доступ к n
, — это этот экземпляр объекта счетчика и методы на нем.
Обратите внимание, что ссылка на n
активна, и каждый вызов counter создает новую область, независимую от областей, созданных предыдущими вызовами, и новую частную переменную в этой области. Так что n
для counter1
может не соответствовать n
для counter2
.
Частичное применение
Частичное приложение — это функция, к которой были применены некоторые, но не все ее аргументы. Давайте посмотрим на пример:
const trace = label => value => {
console.log(`${ label }: ${ value }`);
};
trace
— это функция, которая принимает метку и значение и записывает их в консоль.
Поскольку эта функция является каррированной, мы можем создавать специальные «подфункции», которые являются частичными приложениями полной функции трассировки:
const traceLabelX = trace('Label X')
console.log(traceLabelX.toString()) // 'value => {console.log(`${label}: ${value}`);}'
traceLabelX(20) // 'Label X : 20'
Если вы зарегистрируете traceLabelX
в консоли, вы увидите, что она возвращает функцию, которая принимает значение и регистрирует метку и значение. Но где label
? Закрытие этой функции имеет доступ к label
, с которым оно было возвращено, везде, где оно сейчас используется.
Слушатели событий
Откройте VSCode, создайте эту маленькую страницу .html
и откройте ее в браузере.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> Closures in event listeners </body>
<script> const body = document.body; const initButtons = () => { let button; for (var i = 0; i < 5; i++) { button = document.createElement("button"); button.innerHTML = "Button " + i; button.addEventListener("click", (e) => { alert(i); }); body.appendChild(button); } }; initButtons(); </script> </html>
Как вы думаете, что происходит, когда вы нажимаете на кнопки? Каждое нажатие кнопки возвращает предупреждение с цифрой «5». Почему это? Первое, на что следует обратить внимание, это то, что мы используем var
, а не let
для объявления i
. Таким образом, это немного надуманный пример, поскольку в наши дни вы очень редко используете var
для объявления переменных, но держитесь меня, поскольку это поможет вам понять замыкания. Помните: var
относится к функциям, а let
— к блокам.
Цикл for
находится внутри функции initButtons
, а var
"поднят" на вершину функции.
Каждый раз, когда цикл завершается, создается кнопка с прикрепленным прослушивателем событий, обратный вызов которого имеет ссылку на i
. По мере завершения последующих циклов i
обновляется, как и каждый прослушиватель событий, ссылающийся на него. В этом проблема, каждое замыкание имеет доступ к одной и той же ссылке на i
.
Мы можем исправить это несколькими способами:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> Closures in event listeners </body>
<script> const body = document.body;
const initButton = (name, alertMessage) => { button = document.createElement("button"); button.innerHTML = "Button " + name; button.addEventListener("click", (e) => { alert(alertMessage); }); body.appendChild(button); };
for (var i = 0; i < 5; i++) { initButton(i, i); } </script> </html>
Каждый прослушиватель событий теперь привязан к параметру alertMessage
, который определяется при вызове функции.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> Closures in event listeners </body>
<script> const body = document.body;
const initButtons = () => { let button;
for (let i = 0; i < 5; i++) { button = document.createElement("button"); button.innerHTML = "Button " + i; button.addEventListener("click", (e) => { alert(i); }); body.appendChild(button); } }; initButtons(); </script> </html>
Или просто используйте let
вместо var
внутри цикла. Использование let
гарантирует, что каждая итерация области будет иметь свою независимую привязку i
.
Это помогло вам понять закрытие? Дай мне знать в комментариях!
использованная литература
- https://medium.com/javascript-scene/master-the-javascript-interview-what-is-a-closure-b2f0d2152b36#.11d4u33p7
- https://medium.com/javascript-scene/curry-and-function-composition-2c208d774983
- JavaScript: Полное руководство, 7-е издание Дэвида Фланагана