Замыкание – это функция, связанная со своим лексическим окружением

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.

Это помогло вам понять закрытие? Дай мне знать в комментариях!

использованная литература

  1. https://medium.com/javascript-scene/master-the-javascript-interview-what-is-a-closure-b2f0d2152b36#.11d4u33p7
  2. https://medium.com/javascript-scene/curry-and-function-composition-2c208d774983
  3. JavaScript: Полное руководство, 7-е издание Дэвида Фланагана