MDN’s new design is in Beta! A sneak peek: https://blog.mozilla.org/opendesign/mdns-new-design-beta/

Замыкания — это функции, ссылающиеся на независимые (свободные) переменные. Другими словами, функция, определённая в замыкании, «запоминает» окружение, в котором она была создана.

Лексическая область видимости

Рассмотрим следующий пример:

function init() {
    var name = "Mozilla"; // name - локальная переменная, созданная в init
    function displayName() { // displayName() - внутренняя функция, замыкание
        alert (name); // displayName() использует переменную, объявленную в родительской функции    
    }
    displayName();    
}
init();

Функция init() создаёт локальную переменную name , а затем вызывает функцию displayName(). displayName() — это внутренняя функция — она определена внутри init() и доступна только внутри тела этой функции. В отличие от init(), displayName() не имеет локальных переменных и вместо этого использует переменную name , определённую в родительской функции.

Выполните этот код и посмотрите, как он работает. Это пример так называемой лексической области видимости (lexical scoping): в JavaScript область действия переменной определяется по её расположению в коде (это очевидно лексически), и вложенные функции имеют доступ к переменным, объявленным вовне. Этот механизм и называется Lexical scoping (область действия, ограниченная лексически).

Замыкание

Рассмотрим следующий пример:

function makeFunc() {
  var name = "Mozilla";
  function displayName() {
    alert(name);
  }
  return displayName;
};

var myFunc = makeFunc();
myFunc();

Если выполнить, это будет иметь такой же эффект, как и init() в предыдущем примере: строка "Mozilla" будет показана в JavaScript alert диалоге. Разница в том, что внутренняя функция displayName() была возвращена из внешней до того, как была выполнена.

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

Решение этой головоломки в том, что myFunc стала замыканием. Замыкание — это особенный вид объекта, который сочетает две вещи: функцию и окружение, в котором функция была создана. Окружение состоит из любой локальной переменной, которая была в области действия функции во время создания замыкания. В этом случае, myFunc — это замыкание, которое содержит и функцию displayName , и строку "Mozilla", которые существовали во время создания замыкания.

Вот более интересный пример — функция makeAdder:

function makeAdder(x) {
  return function(y) {
    return x + y;
  };
};

var add5 = makeAdder(5);
var add10 = makeAdder(10);

console.log(add5(2));  // 7
console.log(add10(2)); // 12

Здесь мы определили функцию makeAdder(x), которая получает x, и возвращает новую функцию. Эта функция получает y, и возвращает сумму x и y.

По существу, makeAdder это фабричная функция — она создает функции,  которые  могут добавлять специфические значения для своих аргументов. В примере выше мы используем нашу фабричную функцию для создания двух новых функций — одна определяет в качестве своего аргумента значение 5, вторая - 10.

add5 и add10 — это примеры замыканий. Тело этих функций одинаково, но при этом они сохраняют различное окружение. В окружении функции add5 x — это 5, в то время как в окружении add10 x — это 10.

Замыкания на практике

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

То есть, замыкания можно использовать везде, где было бы нормальным и правильным использовать объект с одним единственным методом.

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

Давайте рассмотрим практический пример: допустим, мы хотим добавить на страницу несколько кнопок, которые будут менять размер текста. Как вариант, мы можем указать свойство font-size на элементе body в пикселах, а затем устанавливать размер прочих элементов страницы (таких, как заголовки, например) с использованием относительных единиц em:

body {
  font-family: Helvetica, Arial, sans-serif;
  font-size: 12px;
}

h1 {
  font-size: 1.5em;
}

h2 {
  font-size: 1.2em;
}

Тогда наши кнопки будут менять свойство font-size элемента body, а остальные элементы страницы просто подцепят это новое значение и отмасштабируют размер текста благодаря использованию относительных единиц.

Используем следующий JavaScript:

function makeSizer(size) {
  return function() {
    document.body.style.fontSize = size + 'px';
  };
};

var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);

Теперь size12, size14, и size16 - это функции, которые меняют размер текста в элементе body на значения 12, 14, и 16 пикселов, соответственно. После чего мы цепляем эти функции на кнопки примерно так:

document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;
<a href="#" id="size-12">12</a>
<a href="#" id="size-14">14</a>
<a href="#" id="size-16">16</a> 

Посмотреть на JSFiddle

 

Эмуляция приватных методов с помощью замыканий

Языки вроде Java позволяют нам объявлять методы приватными. Это когда их могут вызывать только другие методы того же класса, а снаружи они не доступны.

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

Вот как можно описать с помощью замыканий несколько публичных методов, которые имеют доступ к приватным методам и переменным. Такая манера программирования еще называется module pattern. В русском языке устоявшегося перевода до сих пор нет, но можно использовать поиск слов "модуль", "шаблон" и "Javacript":

var Counter = (function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  };   
})();

alert(Counter.value()); /* Alerts 0 */
Counter.increment();
Counter.increment();
alert(Counter.value()); /* Alerts 2 */
Counter.decrement();
alert(Counter.value()); /* Alerts 1 */

Тут много чего поменялось. В предыдущем примере каждое замыкание имело свой собственный контекст исполнения (окружение). Здесь мы создаем единое окружение для трех функций: Counter.increment, Counter.decrement, и Counter.value.

Единое окружение создается в теле анонимной функции, которая исполняется в момент описания. Это окружение содержит два приватных элемента: переменную privateCounter и фукцию changeBy. Ни один из этих элементов не доступен напрямую, за пределами этой самой анонимной функции. Вместо этого они могут и должны использоваться тремя публичными функциями, которые возвращаются анонимным блоком кода (anonymous wrapper), выполняемым в той же анонимной функции.

Эти три публичные функции являются замыканиями, использующими общий контекст исполнения (окружение). Благодаря механизму lexical scoping в Javascript, все они имеют доступ к переменной privateCounter и функции changeBy.

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

var makeCounter = function() {
  var privateCounter = 0;
  function changeBy(val) {
    privateCounter += val;
  }
  return {
    increment: function() {
      changeBy(1);
    },
    decrement: function() {
      changeBy(-1);
    },
    value: function() {
      return privateCounter;
    }
  }  
};

var Counter1 = makeCounter();
var Counter2 = makeCounter();
alert(Counter1.value()); /* Alerts 0 */
Counter1.increment();
Counter1.increment();
alert(Counter1.value()); /* Alerts 2 */
Counter1.decrement();
alert(Counter1.value()); /* Alerts 1 */
alert(Counter2.value()); /* Alerts 0 */

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

Используя замыкания подобным образом, вы получаете ряд преимуществ, обычно ассоциируемых с объектно-ориентированным программированием, таких как изоляция и инкапсуляция.

Создание замыканий в цикле: Очень частая ошибка.

До того, как в версии ECMAScript 6 ввели ключевое слово let , постоянно возникала следующая проблема при создании замыканий внутри цикла. Рассмотрим следующий пример:

<p id="help">Helpful notes will appear here</p>
<p>E-mail: <input type="text" id="email" name="email"></p>
<p>Name: <input type="text" id="name" name="name"></p>
<p>Age: <input type="text" id="age" name="age"></p>
function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Ваш адрес e-mail'},
      {'id': 'name', 'help': 'Ваше полное имя'},
      {'id': 'age', 'help': 'Ваш возраст (Вам должно быть больше 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

setupHelp(); 

Посмотреть на JSFiddle

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

Если вы запустите этот код, то увидите, что он работает не так, как мы хотели. Какое поле вы бы не выбрали, в качестве подсказки всегда будет высвечиваться сообщение насчет возраста. 

Проблема в том, что функции, присвоенные как обработчики события onfocus, являются замыканиями. Они состоят из описания функции и контекста исполнения (окружения), унаследованного от  функции setupHelp. Было создано три замыкания, но все они были созданы с одним и тем же контекстом исполнения (окружением). К моменту возникновения события onfocus цикл уже давно отработал, а значит переменная item (одна и та же для всех трех замыканий) указывает на последний элемент массива, который как раз о поле возраста.

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

function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function makeHelpCallback(help) {
  return function() {
    showHelp(help);
  };
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Ваш адрес e-mail'},
      {'id': 'name', 'help': 'Ваше полное имя'},
      {'id': 'age', 'help': 'Ваш возраст (Вам должно быть больше 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = makeHelpCallback(item.help);
  }
}

setupHelp(); 

Посмотреть на JSFiddle

Вот это работает как следует. Вместо того, чтобы делить на всех одно окружение, функция makeHelpCallback создает каждому из замыканий свое собственное, в котором переменная item указывает на правильный элемент массива helpText.

Соображения по производительности

Не нужно без необходимости создавать функции внутри функций в тех случаях, когда замыкания не нужны. Использование этой техники увеличивает требования к производительности как в части скорости, так и в части потребления памяти.

Как пример, при создании нового объекта/класса есть смысл помещать все методы в прототип этого объекта, а не описывать их в тексте конструктора. Дело в том, что если сделать по-другому, то при каждом создании объекта для него будет создан свой экземпляр каждого из методов, вместо того, чтобы наследовать их из прототипа.

Давайте рассмотрим не очень практичный, но показательный пример:

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
  this.getName = function() {
    return this.name;
  };

  this.getMessage = function() {
    return this.message;
  };
}

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

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype = {
  getName: function() {
    return this.name;
  },
  getMessage: function() {
    return this.message;
  }
};

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

function MyObject(name, message) {
  this.name = name.toString();
  this.message = message.toString();
}
MyObject.prototype.getName = function() {
  return this.name;
};
MyObject.prototype.getMessage = function() {
  return this.message;
};

Код выше можно сделать аккуратнее, выдавая тот же результат:

function MyObject(name, message) {
    this.name = name.toString();
    this.message = message.toString();
}
(function() {
    this.getName = function() {
        return this.name;
    };
    this.getMessage = function() {
        return this.message;
    };
}).call(MyObject.prototype);

В обоих примерах выше методы определяются один раз — в прототипе. И все объекты, использующие данный прототип, будут использовать это определение без дополнительного расхода вычислительных ресурсов. Смотрите подробное описание в статье Подробнее об объектной модели.

Метки документа и участники

Метки: 
 Обновлялась последний раз: sersalex,