JavaScript - Что такое замыкание? Замыкания Соображения по производительности.

Замыкание - это комбинация функции и лексического окружения, в котором эта функция была определена.

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

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

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

init() создаёт локальную переменную name и определяет функцию displayName() . displayName() - это внутренняя функция - она определена внутри init() и доступна только внутри тела функции init() . Обратите внимание, что функция displayName() не имеет никаких собственных локальных переменных. Однако, поскольку внутренние функции имеют доступ к переменным внешних функций, displayName() может иметь доступ к переменной name , объявленной в родительской функции init() .

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(val) . Ни один из этих элементов не доступен напрямую, за пределами этой самой анонимной функции. Вместо этого они могут и должны использоваться тремя публичными функциями, которые возвращаются анонимным блоком кода (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 , постоянно возникала следующая проблема при создании замыканий внутри цикла. Рассмотрим пример:

Helpful notes will appear here

E-mail:

Name:

Age:

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();

Массив 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();

Вот это работает как следует. Вместо того, чтобы делить на всех одно окружение, функция 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) ;

Последнее обновление: 30.03.2018

Замыкания

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

Замыкание технически включает три компонента:

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

    переменные (лексическое окружение), которые определены во внешней функции

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

function outer(){ // внешняя функция var n; // некоторая переменная return inner(){ // вложенная функция // действия с переменной n } }

Рассмотрим замыкания на простейшем примере:

Function outer(){ let x = 5; function inner(){ x++; console.log(x); }; return inner; } let fn = outer(); // fn = inner, так как функция outer возвращает функцию inner // вызываем внутреннюю функцию inner fn(); // 6 fn(); // 7 fn(); // 8

Здесь функция outer задает область видимости, в которой определены внутренняя функция inner и переменная x. Переменная x представляет лексическое окружение для функции inner. В самой функции inner инкрементируем переменную x и выводим ее значение на консоль. В конце функция outer возвращает функцию inner.

Let fn = outer();

Поскольку функция outer возвращает функцию inner, то переменная fn будет хранить ссылку на функцию inner. При этом эта функция запомнила свое окружение - то есть внешнюю переменную x.

Fn(); // 6 fn(); // 7 fn(); // 8

То есть несмотря на то, что переменная x определена вне функции inner, эта функция запомнила свое окружение и может его использовать, несомотря на то, что она вызывается вне функции outer, в которой была определена. В этом и суть замыканий.

Рассмотрим еще один пример:

Function multiply(n){ var x = n; return function(m){ return x * m;}; } var fn1 = multiply(5); var result1 = fn1(6); // 30 console.log(result1); // 30 var fn2= multiply(4); var result2 = fn2(6); // 24 console.log(result2); // 24

Итак, здесь вызов функции multiply() приводит к вызову другой внутренней функции. Внутренняя же функция:

Function(m){ return x * m;};

запоминает окружение, в котором она была создана, в частности, значение переменной x.

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

То есть fn1 - это замыкание, которое содержит и внутреннюю функцию function(m){ return x * m;} , и переменную x, которая существовала во время создания замыкания.

При создании двух замыканий: fn1 и fn2 , для каждого из этих замыканий создается свое окружение.

При этом важно не запутаться в параметрах. При определении замыкания:

Var fn1 = multiply(5);

Число 5 передается для параметра n функции multiply.

При вызове внутренней функции:

Var result1 = fn1(6);

Число 6 передается для параметра m во внутреннюю функцию function(m){ return x * m;}; .

Также мы можем использовать другой вариант для вызова замыкания:

Function multiply(n){ var x = n; return function(m){ return x * m;}; } var result = multiply(5)(6); // 30 console.log(result);

Самовызывающиеся функции

Обычно определение функции отделяется от ее вызова: сначала мы определяем функцию, а потом вызываем. Но это необязательно. Мы также можем создать такие функции, которые будут вызываться сразу при определении. Такие функции еще называют Immediately Invoked Function Expression (IIFE).

(function(){ console.log("Привет мир"); }()); (function (n){ var result = 1; for(var i=1; i