В JavaScript функции могут быть вложены внутри других функций.
Замыкание это когда внутренняя функция имеет доступ к переменным родительской функции, даже после того как родительская фунция выполнена.
Как вы видите, это становиться интерестным, когда внутренняя функция продолжает существовать в вызове родительской функции. Это произойдет при следующих ситуациях:
- внутренняя функция используется как вызов для асинхронной задачи, такой как таймер, событие или AJAX.
- родительская функция возвращает внутреннюю функцию или объект, хранящий внутреннюю функцию.
Замыкание и таймеры
В следующем примере, мы ожидаем что локальная переменная x будет немедленно уничтожена после выполнения функции autorun(), но она будет жива в течении 10 минут. Это связано с тем, что переменная x ипользуется внутренней функцией log(). Функция log() является замыканием.
(function autorun(){
var x = 1;
setTimeout(function log(){
console.log(x);
}, 6000);
})();
Когда используется setInterval(), переменная, на которую ссылается функция замыкания, будет уничтожена только после вызова clearInterval().
Замыкание и события
Мы создаем замыкания каждый раз, когда переменные из внешних функций используются в обработчиках событий. Обработчик событий increment() является замыканием в следующем примере.
(function initEvents(){
var state = 0;
$("#add").on("click", function increment(){
state += 1;
console.log(state);
});
})();
Замыкание и асинхронные задачи
Когда используются переменные из внешней функции при асинхронном вызове, то вызов становится замыканием, а переменные останутся активными до завершения асинхронной задачи.
Таймеры, события и AJAX вызовы, возможно, являются наиболее распространенными асинхронными задачами, но есть и другие примеры: HTML5 Geolocation API, API WebSockets и requestAnimationFrame().
В следующем примере AJAX вызов updateList() является замыканием.
(function init(){
var list;
$.ajax({ url: "https://jsonplaceholder.typicode.com/users"})
.done(function updateList(data){
list = data;
console.log(list);
})
})();
Замыкание и инкапсуляция
Другой способ увидеть замыкание — это функция с приватным состоянием. Замыкание инкапсулирует состояние.
Например, давайте создадим функцию count() с привытным состоянием. Каждый раз, когда она вызывается, она запоминает свое предыдущее состояние и возвращает следующее последовательное число. Переменная state является приватной, доступ к ней извне отсутствует.
function createCount(){
var state = 0;
return function count(){
state += 1;
return state;
}
}
var count = createCount();
console.log(count()); //1
console.log(count()); //2
Мы можем создать множество замыканий, разделяющих одно и то же приватное состояние. В следующем примере increment() и decrement() — это два замыкания, разделяющие одну и ту же переменную приватного состояния. Таким образом, мы можем создавать объекты с приватным состоянием.
function Counter(){
var state = 0;
function increment(){
state += 1;
return state;
}
function decrement(){
state -= 1;
return state;
}
return {
increment,
decrement
}
}
var counter = Counter();
console.log(counter.increment());//1
console.log(counter.decrement());//0
Замыкание против чистых функций
Замыкание используют переменные из внешней области видимости.
Чистые функции не используют переменные из внешней области видимости. Чистые функции должны возвращать значение, вычисленное с использованием только переданных ему значений, и не может быть никаких побочных эффектов, но это практика для подражания.
Асинхронные задачи замыкание и циклы
В следующем примере я создам пять замыканий для пяти асинхронных задач, причем все используют одну и ту же переменную i. Поскольку переменная i изменяется во время цикла, все console.log() отображают одно и то же значение — последнее.
(function run(){
var i=0;
for(i=0; i< 5; i++){
setTimeout(function logValue(){
console.log(i); //5
}, 100);
}
})();
Один из способов устранить эту проблему — использовать IIFE (Immediately Invoked Function Expression). В следующем примере есть еще пять замыканий, но более пяти разных переменных i.
(function run(){
var i=0;
for(i=0; i<5; i++){
(function autorunInANewContext(i){
setTimeout(function logValue(){
console.log(i); //0 1 2 3 4
}, 100);
})(i);
}
})();
Другой вариант — использовать новый способ объявления переменных: через let, доступный как часть ECMAScript 6. Это позволит создать переменную локально для области видимости блока на каждой итерации.
(function run(){
for(let i=0; i<5; i++){
setTimeout(function logValue(){
console.log(i); //0 1 2 3 4
}, 100);
}
})();
Я считаю, что это лучший вариант для этой проблемы с точки зрения читаемости.
Замыкание и сборщик мусора
В JavaScript локальные переменные функции будут уничтожены после возвращения функции, если на них нет по крайней мере ни одной ссылки. Приватное состояние замыкания становится подходящим для сбора мусора после того, как само было убрано. Чтобы сделать это возможным, у замыкания не должно быть больше ссылки.
В следующем примере я сначала создаю замыкание add()
function createAddClosure(){
var arr = [];
return function add(obj){
arr.push(obj);
}
}
var add = createAddClosure();
Затем определяю две функции: одну для добавления большого количества объектов addALotOfObjects() и еще одну clearAllObjects() для задания ссылки null. Затем обе функции используются как обработчики событий.
function addALotOfObjects(){
for(let i=0; i<50000;i++) {
add({fname : i, lname : i});
}
}
function clearAllObjects(){
if(add){
add = null;
}
}
$("#add").click(addALotOfObjects);
$("#clear").click(clearAllObjects);
Нажатие «Add» добавит 50000 предметов в приватное состояние замыкания.
Я нажал «Add» три раза, а затем нажал «Clear», чтобы установить ссылку null. После этого приватное состояние очищается.
Вывод
Замыкание — лучший инструмент в нашей коробке с инструментами для создания инкапсуляции. Это также упрощает нашу работу с вызовами для асинхронных задач. Мы просто используем переменные, которые хотим, и они оживут к моменту вызова.
С другой стороны, очень полезно понять, как работают замыкания, чтобы убедиться, что они и их приватное состояние чистое, когда мы больше не нуждаемся в них.
Это, возможно, лучшая функция, когда-либо помещенная в язык программирования.
Дуглас Крокфорд о замыкании.
Комментарии (23)
NoRegrets
05.04.2018 12:22+3Заголовок такой, что как будто на замыкания забили и автор оригинала решил призвать взглянуть на них еще раз. Жду когда он накатает холиварную статью про сравнение ложки и вилки с призывом «дать еще один шанс ложке».
Aries_ua
05.04.2018 19:33Стоит так же добавить, что замыкания это основная концепция для создания модулей. Ну по крайней мере до появления import/export.
i360u
06.04.2018 13:01-1Меня всегда удивляло то, почему такая "ежу понятная" вещь требует каких-то особых разъяснений. Статьи вот люди пишут… Я бы даже название отдельное не придумывал, понятия "область видимости" вполне достаточно.
mayorovp
Неправда ваша. Ничего не мешает чистой функции использовать переменную из внешней области видимости если та никогда не меняется.
RidgeA
а если другая «грязная» фнукция поменяла значение этой переменной, то результатт «чистой» поменяется, что испачкает функцию
mayorovp
RidgeA
глаз зацепился за «переменную» :-)
dom1n1k
Тогда это константа. Если переменная может меняться хотя бы теоретически — гарантировать чистоту функции уже нельзя.
mayorovp
Константа — это переменная, которая объявлена с ключевым словом const (еще иногда константой называют свойство объекта с параметрами десткриптора
{ writable: false, configurable: false }
).А для того чтобы переменная не менялась — достаточно того чтобы в программе не было оператора присваивания где она находится слева.
mayorovp
Хорошо, зайду с другой стороны. Рассмотрим простейший код с двумя функциями:
Несколько вопросов:
y => x+y
чистой?Еще веселый пример:
Является ли переменная i константой? Является ли функция
x => x+i
чистой?i360u
Нет и нет. А что, тут есть какие-то сомнения? Если i теоретически может быть переопределена в контексте внешнего цикла — ни о какой чистоте не может быть речи. Если передать i как аргумент — все будет чисто. Результат работы "чистой" функции может зависеть ТОЛЬКО от переданных в нее аргументов, непосредственно.
mayorovp
Почему вы теоретически рассматриваете возможность появления новой строки кода снаружи функции — но не рассматриваете возможности появления таковой строки внутри?
Эдак можно про любую функцию сказать "Пока существует теоретическая возможность добавления нечистых операторов в функцию — она не может считаться чистой!" :-)
Чистота функции — это свойство конкретной функции в конкретном коде.
i360u
Потому, что появление этой строки внутри — это часть внутренней логики, суть самой функции. А все что снаружи — это окружение, которое не должно влиять на эту логику. Строки могут появиться где угодно, но сама суть концепции чистых функций — это защита от внешних факторов в тех местах, где это важно. Вы же, предлагаете считать часть внешней среды безопасной по дефолту но не можете дать никакой гарантии этой безопасности.
mayorovp
Вы так говорите как будто внешний и внутренний код два разных человека пишут…
В любом случае, у термина «чистая функция» есть определение, и там ничего не сказано про код и гарантии, только про наблюдаемое поведение.
i360u
Вы никогда не участвовали в командной разработке? Тогда понятно.
Все там сказано. Более того, там сказано, что чистая функция и сама не должна косвенным образом влиять на окружение, т. е. не обладать "побочными эффектами".
mayorovp
Рассмотрим снова код
const add = x => y => x+y;
. Какие два разных человека его писали?Там сказано следующее: чистая функция — это детерминированная функция без побочных эффектов. Детерминированная функция — это функция, которая всегда возвращает одинаковое значения для одинаковых входных параметров.
Так вот: функция
add(5)
детерминирована, независимо от того нравится вам это или нет.i360u
Я не очень понимаю что именно вы пытаетесь проиллюстрировать своим примером. Ваша функция add вернет грязную функцию, потому, что возьмет значение x из контекста add и будет зависеть от. Если вы вернете не стрелочную функцию — она также будет грязной, так как будет искать значение "x" в контексте своего вызова. Саму функцию add — можно с натяжкой назвать чистой, потому при одинаковом "x" будет возвращать функцию выполняющую то-же действие, но по сути это будут разные объекты, тут уже всплывает специфика языка и то, что вы назовете "детерминированным" в контексте вашей задачи. По сути, никакого концептуального отношения к рассматриваемому вопросу тут нет вообще, вы приводите извращенные примеры и пытаетесь заставить кого-то с ними разбираться, а на практике — вам просто по рукам надают за такой код.
mayorovp
Вот только функция add — чистая. Это же всего лишь каррированный оператор сложения. А операция каррирования чистоту функции не меняет.
Если послушать вас, то в том же Haskell чистых функций нескольких аргументов не бывает в принципе…
dom1n1k
Функция add действительно чистая, но вот остальные функции в обсуждаемых фрагментах грязные.
Да, чистая функция может содержать внутри себя грязную. Это возможно, но считается плохой практикой.
mayorovp
По сути же ваших рассуждений:
Обоснуйте. Рассмотрим, для определенности, функцию
const f = add(5)
.Приведите контрпример к утверждению "f — чистая функция". Попробуйте найти сценарий, при котором функция f, вызванная с одним и тем же аргументом, вернет два разных значения.
i360u
А теперь еще разок перечитайте то, что сами процитировали. Хотя не нужно, не вижу больше смысла переубеждать вас.
mayorovp
Кстати, не могли бы вы привести пример чистой функции из реального проекта, которая никак не зависит от окружения?