Статьи

Демистификация JavaScript-замыканий, обратных вызовов и IIFE

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

Затворы

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

Это означает, что практически любую функцию можно считать закрытием, потому что, как мы узнали в разделе « Область действия переменных » в первой части этого урока, функция может ссылаться или иметь доступ к —

  • любые переменные и параметры в своей области видимости функции
  • любые переменные и параметры внешних (родительских) функций
  • любые переменные из глобальной области видимости.

Так что, скорее всего, вы уже использовали замыкания, даже не подозревая об этом. Но наша цель не просто использовать их — это понять их. Если мы не понимаем, как они работают, мы не можем их правильно использовать. По этой причине мы собираемся разделить приведенное выше определение замыкания на три простых для понимания момента.

Точка 1: Вы можете ссылаться на переменные, определенные вне текущей функции.

function setLocation(city) { var country = "France"; function printLocation() { console.log("You are in " + city + ", " + country); } printLocation(); } setLocation ("Paris"); // output: You are in Paris, France 

Попробуйте пример в JS Bin

В этом примере printLocation() функция printLocation() ссылается на переменную country и параметр city включающей (родительской) функции setLocation() . В результате, когда setLocation() , printLocation() успешно использует переменные и параметры первого для вывода «Вы находитесь в Париже, Франция».

Пункт 2: Внутренние функции могут ссылаться на переменные, определенные во внешних функциях, даже после того, как последние вернулись.

 function setLocation(city) { var country = "France"; function printLocation() { console.log("You are in " + city + ", " + country); } return printLocation; } var currentLocation = setLocation ("Paris"); currentLocation(); // output: You are in Paris, France 

Попробуйте пример в JS Bin

Это почти идентично первому примеру, за исключением того, что на этот раз printLocation() возвращается внутри внешней функции setLocation() , а не setLocation() немедленно. Итак, значением currentLocation является внутренняя printLocation() .

Если мы предупреждаем currentLocation следующим образом — alert(currentLocation); — мы получим следующий вывод:

 function printLocation () { console.log("You are in " + city + ", " + country); } 

Как мы видим, printLocation() выполняется за пределами своей лексической области. Кажется, что setLocation() больше нет, но printLocation() все еще имеет доступ и «запоминает» свою переменную ( country ) и параметр ( city ).

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

Точка 3: Внутренние функции хранят переменные своей внешней функции по ссылке, а не по значению.

 function cityLocation() { var city = "Paris"; return { get: function() { console.log(city); }, set: function(newCity) { city = newCity; } }; } var myLocation = cityLocation(); myLocation.get(); // output: Paris myLocation.set('Sydney'); myLocation.get(); // output: Sydney 

Попробуйте пример в JS Bin

Здесь cityLocation() возвращает объект, содержащий два замыкания — get() и set() — и они оба ссылаются на внешнюю переменную city . get() получает текущее значение city , а set() обновляет его. Когда myLocation.get() вызывается во второй раз, он выводит обновленное (текущее) значение city — «Сидней» — вместо значения «Париж» по умолчанию.

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

Одна интересная особенность замыканий состоит в том, что переменные в замыкании автоматически скрываются. Замыкания хранят данные в своих вложенных переменных, не предоставляя прямой доступ к ним. Единственный способ изменить эти переменные — предоставить им косвенный доступ. Например, в последнем фрагменте кода мы увидели, что мы можем изменять переменную city только косвенно, используя замыкания get() и set() .

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

Как видите, вокруг замыканий нет ничего мистического или эзотерического — нужно запомнить только три простых момента.

Callbacks

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

Функция, которая принимает другие функции в качестве аргументов или возвращает функции в качестве своего результата, называется функцией более высокого порядка , а функция, переданная в качестве аргумента, называется функцией обратного вызова . Он называется «обратный вызов», потому что в определенный момент времени он «вызывается обратно» функцией высшего порядка.

Обратные вызовы имеют много повседневных привычек. Один из них — когда мы используем методы setTimeout() и setInterval() объекта window браузера — методы, которые принимают и выполняют обратные вызовы:

 function showMessage(message){ setTimeout(function(){ alert(message); }, 3000); } showMessage('Function called 3 seconds ago'); 

Попробуйте пример в JS Bin

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

 // HTML <button id='btn'>Click me</button> // JavaScript function showMessage(){ alert('Woohoo!'); } var el = document.getElementById("btn"); el.addEventListener("click", showMessage); 

Попробуйте пример в JS Bin

Самый простой способ понять, как работают функции высокого уровня и обратные вызовы, — это создать свои собственные. Итак, давайте создадим один сейчас:

 function fullName(firstName, lastName, callback){ console.log("My name is " + firstName + " " + lastName); callback(lastName); } var greeting = function(ln){ console.log('Welcome Mr. ' + ln); }; fullName("Jackie", "Chan", greeting); 

Попробуйте пример в JS Bin

Здесь мы создаем функцию fullName() которая принимает три аргумента — два для имени и фамилии и один для функции обратного вызова. Затем, после оператора console.log() , мы помещаем вызов функции, которая вызовет реальную функцию обратного вызова — функцию greeting() определенную ниже fullName() . И, наконец, мы вызываем fullName() , где greeting() передается как переменная — без скобок — потому что мы не хотим, чтобы она выполнялась сразу, а просто хотим указать на нее для последующего использования fullName() .

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

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

 function fullName(firstName, lastName, callback){ console.log("My name is " + firstName + " " + lastName); callback(lastName); } fullName("Jackie", "Chan", function(ln){console.log('Welcome Mr. ' + ln);}); 

Попробуйте пример в JS Bin

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

Допустим, нам нужны две функции — одна печатает информацию об опубликованных статьях, а другая — информацию об отправленных сообщениях. Мы создаем их, но мы замечаем, что некоторая часть нашей логики повторяется в обеих функциях. Мы знаем, что иметь один и тот же кусок кода в разных местах не нужно и его сложно поддерживать. Итак, каково решение? Давайте проиллюстрируем это в следующем примере:

 function publish(item, author, callback){ // Generic function with common data console.log(item); var date = new Date(); callback(author, date); } function messages(author, time){ // Callback function with specific data var sendTime = time.toLocaleTimeString(); console.log("Sent from " + author + " at " + sendTime); } function articles(author, date){ // Callback function with specific data var pubDate = date.toDateString(); console.log("Written by " + author); console.log("Published " + pubDate); } publish("How are you?", "Monique", messages); publish("10 Tips for JavaScript Developers", "Jane Doe", articles); 

Попробуйте пример в JS Bin

То, что мы здесь сделали, — это поместили шаблон повторяющегося кода ( console.log(item) и var date = new Date() ) в отдельную обобщенную функцию ( publish() ) и оставили только определенные данные внутри других функций. — которые сейчас являются обратными вызовами. Таким образом, с помощью одной и той же функции мы можем печатать информацию для всех видов связанных вещей — сообщений, статей, книг, журналов и так далее. Единственное, что вам нужно сделать, — это создать специализированную функцию обратного вызова для каждого типа и передать ее в качестве аргумента функции publish() .

Выражения с немедленным вызовом функций (IIFE)

Выражение немедленного вызова функции , или IIFE (произносится как «iffy»), является выражением функции (именованным или анонимным), которое выполняется сразу после его создания.

Существует два слегка отличающихся синтаксических варианта этого шаблона:

 // variant 1 (function () { alert('Woohoo!'); })(); // variant 2 (function () { alert('Woohoo!'); }()); 

Чтобы превратить обычную функцию в IIFE, вам нужно выполнить два шага:

  1. Вы должны заключить всю функцию в скобки. Как следует из названия, IIFE должен быть выражением функции, а не определением функции. Итак, цель заключенных в скобки — преобразовать определение функции в выражение. Это связано с тем, что в JavaScript все в скобках рассматривается как выражение.
  2. Вам нужно добавить пару скобок в самом конце (вариант 1) или сразу после закрывающей фигурной скобки (вариант 2), что приведет к немедленному выполнению функции.

Есть также еще три вещи, которые следует иметь в виду:

Во-первых, если вы назначаете функцию переменной, вам не нужно заключать всю функцию в скобки, потому что это уже выражение:

 var sayWoohoo = function () { alert('Woohoo!'); }(); 

Во-вторых, в конце IIFE требуется точка с запятой, иначе ваш код может работать некорректно.

И в-третьих, вы можете передавать аргументы в IIFE (в конце концов, это функция), как показано в следующем примере:

 (function (name, profession) { console.log("My name is " + name + ". I'm an " + profession + "."); })("Jackie Chan", "actor"); // output: My name is Jackie Chan. I'm an actor. 

Попробуйте пример в JS Bin

Это распространенный способ передачи глобального объекта в качестве аргумента IIFE, чтобы он был доступен внутри функции без необходимости использования объекта window , что делает код независимым от среды браузера. Следующий код создает переменную global которая будет ссылаться на глобальный объект независимо от того, на какой платформе вы работаете:

 (function (global) { // access the global object via 'global' })(this); </code></pre> <p>This code will work both in the browser (where the global object is <code>window</code>), or in a Node.js environment (where we refer to the global object with the special variable <code>global</code>). </p> <p>One of the great benefits of an IIFE is that, when using it, you don't have to worry about polluting the global space with temporary variables. All the variables you define inside an IIFE will be local. Let's check this out:</p> [code language="javascript"](function(){ var today = new Date(); var currentTime = today.toLocaleTimeString(); console.log(currentTime); // output: the current local time (eg 7:08:52 PM) })(); console.log(currentTime); // output: undefined 

Попробуйте пример в JS Bin

В этом примере первый оператор console.log() работает нормально, но второй сбой, потому что переменные today и currentTime сделаны локальными благодаря IIFE.

Мы уже знаем, что замыкания сохраняют ссылки на внешние переменные и, следовательно, возвращают самые последние / обновленные значения. Итак, что, по вашему мнению, будет выходом из следующего примера?

 function printFruits(fruits){ for (var i = 0; i &lt; fruits.length; i++) { setTimeout( function(){ console.log( fruits[i] ); }, i * 1000 ); } } printFruits(["Lemon", "Orange", "Mango", "Banana"]); 

Попробуйте пример в JS Bin

Возможно, вы ожидали, что названия фруктов будут напечатаны один за другим с интервалом в одну секунду. Но на практике результат в четыре раза «не определен». Так где же подвох?

Уловка в том, что значение i внутри оператора console.log() равно 4 для каждой итерации цикла. И, поскольку у нас нет ничего в индексе 4 в нашем массиве фруктов, вывод «неопределен». (Помните, что в JavaScript индекс массива начинается с 0.) Цикл завершается, когда i < fruits.length возвращает false . Итак, в конце цикла значение i равно 4. Эта самая последняя версия переменной используется во всех функциях, создаваемых циклом. Все это происходит потому, что замыкания связаны с самими переменными, а не с их значениями.

Чтобы решить эту проблему, нам нужно предоставить новую область видимости — для каждой функции, созданной циклом — которая будет фиксировать текущее состояние переменной i . Мы делаем это, закрывая метод setTimeout() в IIFE и определяя личную переменную для хранения текущей копии i .

 function printFruits(fruits){ for (var i = 0; i &lt; fruits.length; i++) { (function(){ var current = i; // define new variable that will hold the current value of "i" setTimeout( function(){ console.log( fruits[current] ); // this time the value of "current" will be different for each iteration }, current * 1000 ); })(); } } printFruits(["Lemon", "Orange", "Mango", "Banana"]); 

Попробуйте пример в JS Bin

Мы также можем использовать следующий вариант, который выполняет ту же работу:

 function printFruits(fruits){ for (var i = 0; i &lt; fruits.length; i++) { (function(current){ setTimeout( function(){ console.log( fruits[current] ); }, current * 1000 ); })( i ); } } printFruits(["Lemon", "Orange", "Mango", "Banana"]); 

Попробуйте пример в JS Bin

IIFE часто используется для создания области для инкапсуляции модулей. Внутри модуля есть частная область, которая является автономной и защищена от нежелательных или случайных изменений. Этот метод, называемый модульным шаблоном , является мощным примером использования замыканий для управления областью видимости и широко используется во многих современных библиотеках JavaScript (например, jQuery и Underscore).

Вывод

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

Для более подробного и всестороннего объяснения тем, представленных здесь, я рекомендую вам взглянуть на книгу Кайла Симпсона « Не знаю»: «Область применения и замыкания» .