Понимание замыканий в JavaScript

Концепция закрытия - одна из тем в JavaScript, которая может быть сложной для понимания. Однако они могут быть чрезвычайно полезны, если вы их освоите! Они могут помочь вам добавить множество многоразовых функций в любое веб-приложение. Это помогает нам экономить время при компиляции и запуске приложения. Итак, что такое закрытие, вы спрашиваете, я собираюсь объяснить вам, сначала разбив его на более мелкие части и шаг за шагом, чтобы понять, что именно.

Что такое закрытие?

Объяснение от MDN Web Docs:

«Замыкание - это комбинация функции, объединенной (заключенной), со ссылками на ее окружающее состояние (лексическое окружение). Другими словами, замыкание дает вам доступ к области внешней функции из внутренней функции. В JavaScript замыкания создаются каждый раз, когда создается функция, во время создания функции ».

Поговорим об объемах

Области видимости определяют доступность (или видимость) переменных. И есть 2 вида:

  • Локальные области
  • Глобальные области

Локальный охват

Переменные, объявленные в функции JavaScript, относятся к ее ЛОКАЛЬНОЙ области видимости. Доступ к ним можно получить только в этой функции. Локальные переменные создаются при запуске функции и удаляются, когда функция завершается.

function myFunction() {
    var count = 0;
    console.log(count);
};
myFunction(); => 0 
console.log(count); => ReferenceError: count is not defined
                       count is declared within myFunction's 
                       scope, therefore it is not eligible 
                       outside of that function.

Вы даже можете повторно использовать имена общих переменных (count, index, current, value и т. Д.) В разных областях, не вызывая ошибок:

function myFunction() {

    // 'myFunction" function scope
    // This count variable belongs here, and it will log 0

    let count = 0;

    console.log(count);
};

function someFunction(){

    // 'someFunction" function scope
    // This count variable belongs here, and it will log 1

    let count = 1;

    console.log(count);
};

myFunction(); => 0
someFunction(); => 1

Глобальный масштаб

Переменные, объявленные вне функции, относятся к ГЛОБАЛЬНОЙ области видимости. После этого все скрипты и функции на веб-странице могут получить к ней доступ.

let carName = "Volvo";

console.log(carName); => "Volvo"

function myFunction() {

  // Code written inside this function can also use carName, 
     because carName belongs to Global Scope

  console.log(carName);

};

myFunction(); => "Volvo"

Это здорово, но с прицелами мы можем сделать гораздо больше!

Области вложенности

Давайте немного поэкспериментируем и поместим один прицел внутрь другого! Это называется вложением:

function outerFunction(){

    // The outer scope

    let outerVar = "Outer variable";

   function innerFunction(){

       // The inner scope
       // The variables of the outer scope are accessible inside the inner scope 

       console.log(outerVar);

   }

   innerFunction();

}

outerFunction(); => "Outer variable"

Давайте посмотрим на приведенный выше код и пошагово разберемся, как он работает:

  • Функция с именем outerFunction() определяется переменной outerVar и функцией innerFunction() в своем теле.
  • innerFunction() запускает консольный журнал нашего outerVar.
  • Поскольку в innerFuction() нет объявления outerVar, JavaScript будет рассматривать outerVar ту же переменную, объявленную в outerFunction(). Однако, если мы создадим еще один outerVar в нашем innerFunction(), наш innerFunction() сначала будет полагаться на эту переменную.

Вот еще один пример:

let myGlobal = 0;

function myFunction() {

  let myVar = 1;

  console.log(myGlobal); // => It will log 0

  function innerOfFunction() {

    let myInnerVar = 2;

    console.log(myVar, myGlobal); // => I will log 1 0

    function innerOfInnerOfFunction() {

      console.log(myInnerVar, myVar, myGlobal); // => It will log 
                                                      2 1 0 

    }

    innerOfInnerOfFunction();

  }

  innerOfFunction();

}

myFunction(); => 0 
                 1 0
                 2 1 0

Давайте посмотрим на приведенный выше код построчно:

  1. Переменная myGlobal объявлена ​​в Global Scope, и ее значение равно 0.
  2. Объявлен следующий myFunction().
  3. В области видимости myFunction() переменная myVar объявлена ​​со значением 1.
  4. Внутри myFunction() объявлена ​​другая функция innerOfFunction() с переменной myInnerVar, имеющей в области видимости значение 2.
  5. Затем мы объявляем внутри innerofFunction() другую функцию, называемую innerOfinnerOfFunctoion() (имена примеров сложны!), Консоль которой регистрирует переменные: myGlobal, myVar и myInnerVar. Поскольку он вложен как в myFunction(), так и в innerOfFunction(),, он может получить доступ ко всем переменным над ним.
  6. innerOfFunction() затем завершается вызовом innerOfInnerOfFunction().
  7. Наконец, myFunction() закрывается путем вызова innerOfFunction(), который запускает весь код, вложенный в него.

Внешние области не знают о переменных внутри внутренних областей, а внутренние области (если они вложены) знают о переменных во внешних областях.

Так что же на самом деле закрытие?

Лексическая среда позволяет нам получать статический доступ к переменным внешней области видимости. Мы можем продолжать вкладывать переменные и функции друг в друга, но мы еще не создали замыкание. До закрытия остался всего один шаг!

Взгляните на этот код:

function outerFunction(){

  let counter = 0;

  function innerFunction(){

    console.log(counter+=1); 

   }

  innerFunction(); 
}
outerFunction(); // => 1 
outerFunction(); // => 1 counter didn't change because
                       everytime. outerFunction() is called,
                       it sets the counter to 0, and adds 1 to it.

В настоящее время innerFunction() вызывается в outerFunction(). Давайте изменим это и вместо вызова функции просто вернем ее.

Добавим изменения:

function outerFunction(){

    let counter = 0;

    function innerFunction(){

      console.log(counter+=1); // Let's increment the counter

    }

    return innerFunction; // Note that we are not invoking the
                             function, but just returning it. 

}

Возвращая innerFunction(), мы теперь можем сохранить внешнюю функцию в различных переменных, которые будут ее собственным экземпляром этой функции:

let invokeFirst = outerFunction() // => ƒ innerFunction(){

      console.log(counter+=1); // Le…

// outerFunction() invoked the first time

let invokeSecond = outerFunction() // => ƒ innerFunction(){

      console.log(counter+=1); // Le…

// outerFunction() invoked the first time

// By setting the invokeFirst and invokeSecond variables to
   outerFunction, its value becomes result of innerFunction.


invokeFirst() // => 1 invokeFirst() invoked the first time
invokeFirst() // => 2 invokeFirst() invoked the second time
invokeFirst() // => 3 invokeFirst() invoked the third time

invokeSecond() // => 1 invokeSecond() invoked the first time

Давайте посмотрим, что происходит, когда invokeFirst() выполняется в первый раз:

  1. Создается счетчик переменных, значение которого устанавливается на 0 внутри outerFunction().
  2. outerFunction() вызывается и запускается только один раз, когда он сохраняется в переменных invokeFirst() и invokeSecond().
  3. JavaScript знает, что счетчик переменных больше не существует. Поскольку counter является частью outerFunction(), counter будет существовать только тогда, когда outerFunction() находится в процессе выполнения. Поскольку выполнение outerFunction() завершилось задолго до того, как мы вызвали invokeFirst(), любые переменные в области действия внешней функции перестают существовать, и, следовательно, переменная counter больше не существует.

Но как это работает на самом деле?

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

Все еще не понимаете? Проще говоря, когда мы сохраняем наш outerFunction() в новую переменную, он запускается один раз, он устанавливает свои переменные и выполняет то, что находится внутри него, а затем возвращает innerFunction(). Теперь, когда мы вызываем нашу новую переменную как функцию, она запускает только внутреннюю функцию. innerFunction() запоминает все, что outerFunction() ему передано, а затем выполняет свой собственный код. Все, что не было передано innerFunction(), теперь будет за его пределами. Давайте посмотрим, что это значит:

function outerFunction(){

    let counter = 0;

    let anotherVar = "something" // This won't be remembered by
                                    innerFunction(), because 
                                    outerFunction() returns the 
                                    function of innerFunction() 
                                    and innerFunction() doesn't 
                                    use anotherVar. Therefore, 
                                    it will be forgotten.

    function innerFunction(){

      console.log(counter+=1); // Let's increment the counter

   }

    return innerFunction;

}

Популярные варианты использования замыканий

  • Обработка слушателей событий
  • Частные методы
  • Функциональное программирование

Обработка слушателей событий

const myButton = document.getElementById("myButton");
const button = document.getElementById("button");

const myfunc = () => {

  let clicked = 0;

  const inner = () => {

    return clicked +=1;

  };

  return inner;

};

let callback = myfunc();

myButton.addEventListener("click", function handleClick() {

  myText.innerText = `You clicked ${callback()} times`;

});

Чем он здесь полезен?

Здесь каждый раз, когда мы устанавливаем myFunc() в новую переменную, он будет предоставлять нам новое значение clicked, начиная с 0. Это делает его повторно используемым для множества различных прослушивателей событий. Представьте себе добавление лайков на страницу в социальных сетях или функцию аплодисментов.

Частные методы

Мы можем эмулировать частные методы с помощью замыканий. Частные методы предоставляют нам отличный способ управлять нашим глобальным пространством имен и ограничивать доступ к коду.

let counter = (function() {

  let privateCounter = 0;

  function changeBy(val) {

    privateCounter += val;

  }

  return {

    increment: function() {

      changeBy(1);

    },

    decrement: function() {

      changeBy(-1);

    },

    value: function() {

      return privateCounter;

    }

  };

})();

console.log(counter.value());  // 0.

counter.increment();
counter.increment();
console.log(counter.value());  // 2.

counter.decrement();
console.log(counter.value());  // 1.

В приведенном выше коде мы делаем несколько вещей. Сначала мы создаем частную переменную (privateCounter со значением 0) и частную функцию (changeBy(), которая добавляет значение к переменной privateCounter). Оба они являются частными, потому что мы не передаем их нашему утверждению. Во-вторых, в нашем операторе возврата мы создаем анонимную оболочку, которая содержит три общедоступных объекта: increment, decrement и value. Инкремент и декремент вызывают функцию changeBy(), чтобы изменить значение privateCounter на 1 или -1. Значение просто возвращает значение нашего privateCounter. Затем к возвращаемым здесь объектам можно получить доступ, вызвав наш счетчик и вызвав одну из клавиш функции, которую мы хотим запустить (например: counter.value())

Функциональное программирование

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

function division(a) {

  return function runDivision(b) {

    return b / a;

  }
  
}

const divideByTwo= division(2);
divideByTwo(4); // => 2
divideByTwo(6); // => 3

const divideByThree = division(3);
tridivideByThree(3); // => 1
tridivideByThree(9); // => 3

Заключение

Если вы обнаружите, что повторно используете одни и те же блоки кода снова и снова, замыкания - отличный способ создать многоразовые функции, которые можно разместить там, где они вам нужны!

Ресурсы