Привет, Хабр! Представляю вашему вниманию перевод статьи «Why you should give the Closure function another chance» автора Cristi Salcescu.

В 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)


  1. mayorovp
    05.04.2018 10:43
    -1

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

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


    1. RidgeA
      05.04.2018 10:47

      а если другая «грязная» фнукция поменяла значение этой переменной, то результатт «чистой» поменяется, что испачкает функцию


      1. mayorovp
        05.04.2018 10:50

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


        1. RidgeA
          05.04.2018 11:09

          глаз зацепился за «переменную» :-)


        1. dom1n1k
          05.04.2018 11:49

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


          1. mayorovp
            05.04.2018 12:27
            -1

            Константа — это переменная, которая объявлена с ключевым словом const (еще иногда константой называют свойство объекта с параметрами десткриптора { writable: false, configurable: false }).


            А для того чтобы переменная не менялась — достаточно того чтобы в программе не было оператора присваивания где она находится слева.


          1. mayorovp
            05.04.2018 20:35
            +1

            Хорошо, зайду с другой стороны. Рассмотрим простейший код с двумя функциями:


            const add = x => y => x+y;

            Несколько вопросов:


            1. Является ли параметр x константой?
            2. Может ли параметр x меняться теоретически?
            3. Может ли параметр x меняться практически?
            4. Является ли вложенная функция y => x+y чистой?

            Еще веселый пример:


            for (let i=0; i<5; i++) {
                foo(x => x+i);
            }

            Является ли переменная i константой? Является ли функция x => x+i чистой?


            1. i360u
              06.04.2018 12:57

              Нет и нет. А что, тут есть какие-то сомнения? Если i теоретически может быть переопределена в контексте внешнего цикла — ни о какой чистоте не может быть речи. Если передать i как аргумент — все будет чисто. Результат работы "чистой" функции может зависеть ТОЛЬКО от переданных в нее аргументов, непосредственно.


              1. mayorovp
                06.04.2018 13:18

                Почему вы теоретически рассматриваете возможность появления новой строки кода снаружи функции — но не рассматриваете возможности появления таковой строки внутри?


                Эдак можно про любую функцию сказать "Пока существует теоретическая возможность добавления нечистых операторов в функцию — она не может считаться чистой!" :-)


                Чистота функции — это свойство конкретной функции в конкретном коде.


                1. i360u
                  06.04.2018 13:55

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


                  1. mayorovp
                    06.04.2018 13:59

                    Вы так говорите как будто внешний и внутренний код два разных человека пишут…

                    В любом случае, у термина «чистая функция» есть определение, и там ничего не сказано про код и гарантии, только про наблюдаемое поведение.


                    1. i360u
                      06.04.2018 14:13

                      Вы так говорите как будто внешний и внутренний код два разных человека пишут…

                      Вы никогда не участвовали в командной разработке? Тогда понятно.


                      В любом случае, у термина «чистая функция» есть определение, и там ничего не сказано про код и гарантии, только про наблюдаемое поведение.

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


                      1. mayorovp
                        06.04.2018 14:21

                        Рассмотрим снова код const add = x => y => x+y;. Какие два разных человека его писали?


                        Все там сказано.

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


                        Так вот: функция add(5) детерминирована, независимо от того нравится вам это или нет.


                        1. i360u
                          06.04.2018 14:54
                          -1

                          Я не очень понимаю что именно вы пытаетесь проиллюстрировать своим примером. Ваша функция add вернет грязную функцию, потому, что возьмет значение x из контекста add и будет зависеть от. Если вы вернете не стрелочную функцию — она также будет грязной, так как будет искать значение "x" в контексте своего вызова. Саму функцию add — можно с натяжкой назвать чистой, потому при одинаковом "x" будет возвращать функцию выполняющую то-же действие, но по сути это будут разные объекты, тут уже всплывает специфика языка и то, что вы назовете "детерминированным" в контексте вашей задачи. По сути, никакого концептуального отношения к рассматриваемому вопросу тут нет вообще, вы приводите извращенные примеры и пытаетесь заставить кого-то с ними разбираться, а на практике — вам просто по рукам надают за такой код.


                          1. mayorovp
                            06.04.2018 14:59

                            Вот только функция add — чистая. Это же всего лишь каррированный оператор сложения. А операция каррирования чистоту функции не меняет.

                            Если послушать вас, то в том же Haskell чистых функций нескольких аргументов не бывает в принципе…


                            1. dom1n1k
                              07.04.2018 11:28

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

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


                          1. mayorovp
                            06.04.2018 15:02

                            По сути же ваших рассуждений:


                            Ваша функция add вернет грязную функцию, потому, что возьмет значение x из контекста add и будет зависеть от.

                            Обоснуйте. Рассмотрим, для определенности, функцию const f = add(5).


                            Приведите контрпример к утверждению "f — чистая функция". Попробуйте найти сценарий, при котором функция f, вызванная с одним и тем же аргументом, вернет два разных значения.


                            1. i360u
                              06.04.2018 18:22
                              -1

                              А теперь еще разок перечитайте то, что сами процитировали. Хотя не нужно, не вижу больше смысла переубеждать вас.


                  1. mayorovp
                    06.04.2018 14:03

                    Кстати, не могли бы вы привести пример чистой функции из реального проекта, которая никак не зависит от окружения?


  1. dom1n1k
    05.04.2018 11:48

    del


  1. NoRegrets
    05.04.2018 12:22
    +3

    Заголовок такой, что как будто на замыкания забили и автор оригинала решил призвать взглянуть на них еще раз. Жду когда он накатает холиварную статью про сравнение ложки и вилки с призывом «дать еще один шанс ложке».


  1. Aries_ua
    05.04.2018 19:33

    Стоит так же добавить, что замыкания это основная концепция для создания модулей. Ну по крайней мере до появления import/export.


  1. i360u
    06.04.2018 13:01
    -1

    Меня всегда удивляло то, почему такая "ежу понятная" вещь требует каких-то особых разъяснений. Статьи вот люди пишут… Я бы даже название отдельное не придумывал, понятия "область видимости" вполне достаточно.