Статьи

JavaScript закрывает демистификацию

Замыкания — это несколько продвинутая и часто неправильно понимаемая особенность языка JavaScript. Проще говоря, замыкания — это объекты, которые содержат функцию и ссылку на среду, в которой была создана функция. Тем не менее, чтобы полностью понять замыкания, необходимо понять еще две особенности языка JavaScript: первоклассные функции и внутренние функции .

Первоклассные функции

В языках программирования функции считаются гражданами первого класса, если ими можно манипулировать, как и любым другим типом данных. Например, первоклассные функции могут быть созданы во время выполнения и назначены переменным. Они также могут быть переданы и возвращены другими функциями. Помимо соответствия ранее упомянутым критериям, функции JavaScript также имеют свои собственные свойства и методы. В следующем примере показаны некоторые возможности функций первого класса. В этом примере две функции создаются и присваиваются переменным «foo» и «bar». Функция, хранящаяся в «foo», отображает диалоговое окно, а «bar» просто возвращает любой аргумент, переданный ей. Последняя строка примера делает несколько вещей. Во-первых, функция, хранящаяся в «bar», вызывается с «foo» в качестве аргумента. Затем «bar» возвращает ссылку на функцию «foo». Наконец, вызывается возвращенная ссылка «foo», в результате чего отображается «Hello World!».

  var foo = function () {
   alert («Привет, мир!»);
 };

 var bar = function (arg) {
   вернуть аргумент;
 };

 бар (Foo) (); 

Внутренние функции

Внутренние функции, также называемые вложенными функциями, являются функциями, которые определены внутри другой функции (называемой внешней функцией). Каждый раз, когда вызывается внешняя функция, создается экземпляр внутренней функции. В следующем примере показано, как используются внутренние функции. В этом случае add () является внешней функцией. Внутри add () определяется и вызывается внутренняя функция doAdd ().

  функция add (значение1, значение2) {
   function doAdd (операнд1, операнд2) {
     возвратный операнд1 + операнд2;
   }

   return doAdd (значение1, значение2);
 }

 var foo = add (1, 2);
 // foo равно 3 

Одной из важных характеристик внутренних функций является то, что они имеют неявный доступ к области видимости внешней функции. Это означает, что внутренняя функция может использовать переменные, аргументы и т. Д. Внешней функции. В предыдущем примере аргументы «value1» и «value2» метода add () были переданы doAdd () в качестве аргументов «operand1» и «operand2». Однако в этом нет необходимости, поскольку doAdd () имеет прямой доступ к «value1» и «value2». Предыдущий пример был переписан ниже, чтобы показать, как doAdd () может использовать «value1» и «value2».

  функция add (значение1, значение2) {
   function doAdd () {
     возвращаемое значение1 + значение2;
   }

   return doAdd ();
 }

 var foo = add (1, 2);
 // foo равно 3 

Создание замыканий

Закрытие создается, когда внутренняя функция становится доступной извне функции, которая ее создала. Обычно это происходит, когда внешняя функция возвращает внутреннюю функцию. Когда это происходит, внутренняя функция сохраняет ссылку на среду, в которой она была создана. Это означает, что он запоминает все переменные (и их значения), которые находились в области действия в то время. В следующем примере показано, как создается и используется замыкание.

  function add (value1) {
   возвращаемая функция doAdd (value2) {
     возвращаемое значение1 + значение2;
   };
 }

 var increment = add (1);
 var foo = increment (2);
 // foo равно 3 

Есть несколько вещей, которые стоит отметить в этом примере.

  • Функция add () возвращает свою внутреннюю функцию doAdd (). Возвращая ссылку на внутреннюю функцию, создается замыкание.
  • «Value1» — это локальная переменная add () и нелокальная переменная doAdd (). Нелокальные переменные относятся к переменным, которые не входят ни в локальную, ни в глобальную область. «Value2» является локальной переменной doAdd ().
  • Когда вызывается add (1), замыкание создается и сохраняется в «приращении». В ссылочной среде замыкания «значение1» связано со значением один. Связанные переменные также называются закрытыми . Отсюда и название.
  • Когда вызывается приращение (2), вводится замыкание. Это означает, что вызывается doAdd (), а переменная «value1» содержит значение один. По сути, замыкание можно рассматривать как создание следующей функции.
  приращение функции (значение2) {
   вернуть 1 + значение2;
 } 

Когда использовать замыкания

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

Работа с таймерами

Замыкания полезны при использовании вместе с функциями setTimeout () и setInterval () . Чтобы быть более конкретным, замыкания позволяют передавать аргументы в функции обратного вызова setTimeout () и setInterval (). Например, следующий код печатает строку «some message» один раз в секунду, вызывая showMessage ().

  <! DOCTYPE html>
 <html lang = "en">
 <Голова>
   <Название> Затворы </ название>
   <meta charset = "UTF-8" />
   <Скрипт>
     window.addEventListener ("load", function () {
       window.setInterval (showMessage, 1000, «некоторое сообщение <br />»);
     });

     function showMessage (message) {
       document.getElementById ("message"). innerHTML + = message;
     }
   </ Скрипт>
 </ HEAD>
 <Тело>
   <span id = "message"> </ span>
 </ Body>
 </ Html> 

К сожалению, Internet Explorer не поддерживает передачу аргументов обратного вызова через setInterval (). Вместо отображения «некоторого сообщения» в Internet Explorer отображается «неопределенное» (поскольку в showMessage () фактически не передается никакого значения). Чтобы обойти эту проблему, можно создать замыкание, которое связывает аргумент «message» с желаемым значением. Закрытие может затем использоваться как функция обратного вызова для setInterval (). Чтобы проиллюстрировать эту концепцию, код JavaScript из предыдущего примера был переписан ниже для использования замыкания.

  window.addEventListener ("load", function () {
   var showMessage = getClosure ("некоторое сообщение <br />");

   window.setInterval (showMessage, 1000);
 });

 function getClosure (message) {
   function showMessage () {
     document.getElementById ("message"). innerHTML + = message;
   }

   возвращение showMessage;
 } 

Эмуляция личных данных

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

В следующем примере показан конструктор для простого класса Person. Когда каждый человек создается, ему присваивается имя через аргумент «имя». Внутренне Person сохраняет свое имя в переменной «_name». Следуя хорошим методам объектно-ориентированного программирования, для получения имени также предоставляется метод getName ().

  function Person (имя) {
   this._name = name;

   this.getName = function () {
     вернуть this._name;
   };
 } 

Есть еще одна серьезная проблема с классом Person. Поскольку JavaScript не поддерживает личные данные, ничто не мешает кому-то еще прийти и изменить имя. Например, следующий код создает Person с именем Colin, а затем меняет его имя на Tom.

  var person = new Person ("Colin");

 person._name = "Tom";
 // person.getName () теперь возвращает "Tom" 

Лично мне бы не понравилось, если бы кто-нибудь мог прийти и юридически изменить свое имя. Чтобы этого не происходило, можно использовать закрытие, чтобы сделать переменную «_name» закрытой. Конструктор Person был переписан ниже с использованием замыкания. Обратите внимание, что «_name» теперь является локальной переменной конструктора Person вместо свойства объекта. Закрытие формируется потому, что внешняя функция Person () предоставляет внутреннюю функцию, создавая открытый метод getName ().

  function Person (имя) {
   var _name = name;

   this.getName = function () {
     возврат _имя;
   };
 } 

Теперь, когда вызывается getName (), он гарантированно возвращает значение, которое было первоначально передано конструктору. Кто-то еще может добавить новое свойство «_name» к объекту, но внутренняя работа объекта не будет затронута, пока они ссылаются на переменную, связанную закрытием. Следующий код показывает, что переменная «_name» действительно является закрытой.

  var person = new Person ("Colin");

 person._name = "Tom";
 // person._name это "Tom", но person.getName () возвращает "Colin" 

Когда не использовать замыкания

Важно понимать, как работают крышки и когда их использовать. Не менее важно понимать, когда они не являются подходящим инструментом для работы под рукой. Злоупотребление замыканиями может привести к медленному выполнению сценариев и потреблению ненужной памяти. А поскольку замыкания настолько просты в создании, ими можно злоупотреблять, даже не подозревая об этом. Этот раздел охватывает несколько сценариев, в которых замыкания следует использовать с осторожностью.

В петлях

Создание замыканий внутри циклов может привести к ошибочным результатам. Пример этого показан ниже. В этом примере созданы три кнопки. При нажатии «button1» должно отображаться предупреждение «Нажатая кнопка 1». Аналогичные сообщения должны отображаться для «button2» и «button3». Однако при запуске этого кода на всех кнопках отображается «Нажатая кнопка 4». Это связано с тем, что к моменту нажатия одной из кнопок цикл завершается и переменная цикла достигает конечного значения четыре.

  <! DOCTYPE html>
 <html lang = "en">
 <Голова>
   <Название> Затворы </ название>
   <meta charset = "UTF-8" />
   <Скрипт>
     window.addEventListener ("load", function () {
       для (var i = 1; i <4; i ++) {
         var button = document.getElementById ("button" + i);

         button.addEventListener ("click", function () {
           оповещение («Нажатая кнопка» + i);
         });
       }
     });
   </ Скрипт>
 </ HEAD>
 <Тело>
   <input type = "button" id = "button1" value = "One" />
   <input type = "button" id = "button2" value = "Two" />
   <input type = "button" id = "button3" value = "Three" />
 </ Body>
 </ Html> 

Чтобы решить эту проблему, замыкание должно быть отделено от фактической переменной цикла. Это можно сделать, вызвав новую функцию, которая, в свою очередь, создает новую среду ссылок. В следующем примере показано, как это сделать. Переменная цикла передается в функцию getHandler (). Затем getHandler () возвращает замыкание, которое не зависит от исходного цикла «for».

  function getHandler (i) {
   обработчик возвращаемой функции () {
     оповещение («Нажатая кнопка» + i);
   };
 }

 window.addEventListener ("load", function () {
   для (var i = 1; i <4; i ++) {
     var button = document.getElementById ("button" + i);

     button.addEventListener ("click", getHandler (i));
   }
 }); 

Ненужное использование в конструкторах

Функции конструктора являются еще одним распространенным источником неправильного использования замыканий. Мы видели, как можно использовать замыкания для эмуляции личных данных. Тем не менее, чрезмерно сложно реализовывать методы как замыкания, если они на самом деле не обращаются к частным данным. В следующем примере возвращается класс Person, но на этот раз добавляется метод sayHello (), который не использует личные данные.

  function Person (имя) {
   var _name = name;

   this.getName = function () {
     возврат _имя;
   };

   this.sayHello = function () {
     оповещение ( "Привет!");
   };
 } 

Каждый раз, когда создается экземпляр Person, время тратится на создание метода sayHello (). Если создается много объектов Person, это становится пустой тратой времени. Лучшим подходом было бы добавить sayHello () к прототипу Person. При добавлении к прототипу все объекты Person могут использовать один и тот же метод. Это экономит время в конструкторе, не создавая замыкания для каждого экземпляра. Предыдущий пример переписан ниже с посторонним замыканием, перенесенным в прототип.

  function Person (имя) {
   var _name = name;

   this.getName = function () {
     возврат _имя;
   };
 }

 Person.prototype.sayHello = function () {
   оповещение ( "Привет!");
 }; 

То, что нужно запомнить

  • Замыкания содержат функцию и ссылку на среду, в которой была создана функция.
  • Закрытие формируется, когда внешняя функция представляет внутреннюю функцию.
  • Замыкания могут использоваться для простой передачи параметров в функции обратного вызова.
  • Личные данные можно эмулировать с помощью замыканий. Это часто встречается в объектно-ориентированном программировании и проектировании пространства имен.
  • Замыкания не должны быть чрезмерно использованы в конструкторах. Добавление к прототипу — лучшая идея.