Pull to refresh

Почему вы должны дать еще один шанс замыканию

Reading time 4 min
Views 8.3K
Привет, Хабр! Представляю вашему вниманию перевод статьи «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. После этого приватное состояние очищается.



Вывод


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


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


Это, возможно, лучший функционал, когда-либо помещенный в язык программирования.

Дуглас Крокфорд о замыкании.
Tags:
Hubs:
+10
Comments 22
Comments Comments 22

Articles