5

JavaScript: Замыкания против анонимных функций

22

У меня и моего друга в данный момент идет обсуждение, что такое замыкание (closure) в JavaScript и что не является им. Мы хотим убедиться, что мы действительно правильно понимаем этот концепт.

Давайте рассмотрим следующий пример. У нас есть цикл, который считает, и мы хотим вывести значение счетчика с задержкой в консоль. Для этого мы используем setTimeout и замыкания, чтобы захватить значение переменной счетчика и убедиться, что оно не будет напечатано N раз, где N - текущее значение счетчика.

Неправильным решением без замыканий или чего-то, близкого к замыканиям, будет этот код:

for(var i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
}

Что, разумеется, выведет 10 раз значение i после завершения цикла, а именно 10.

Попытка моего друга была следующей:

for(var i = 0; i < 10; i++) {
    (function(){
        var i2 = i;
        setTimeout(function(){
            console.log(i2);
        }, 1000)
    })();
}

Этот код выводит от 0 до 9, как и ожидалось.

Я сказал ему, что он не использует замыкание для захвата i, но он настаивает на том, что использует. Я доказал ему, что он не использует замыкания, поместив тело цикла for внутрь другого setTimeout (передав его анонимную функцию в setTimeout), и в этом случае снова печатается 10 раз 10. То же самое происходит, если я сохраню его функцию в переменной и вызову ее после цикла, также печатая 10 раз 10. Мой аргумент в том, что он на самом деле не захватывает значение i, что делает его версию не замыканием.

Моя попытка была следующей:

for(var i = 0; i < 10; i++) {
    setTimeout((function(i2){
        return function() {
            console.log(i2);
        }
    })(i), 1000);
}

Таким образом, я захватываю i (переименовав его в i2 внутри замыкания), но теперь я возвращаю другую функцию и передаю ее дальше. В моем случае функция, переданная в setTimeout, действительно захватывает i.

Теперь кто использует замыкания, а кто нет?

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

5 ответ(ов)

0

Согласно определению closure:

"Замыкание" — это выражение (обычно функция), которое может содержать свободные переменные, вместе с окружением, которое связывает эти переменные (что "закрывает" выражение).

Вы используете closure, если определяете функцию, которая использует переменную, определённую вне этой функции. (мы называем такую переменную свободной переменной).

Все они используют closure (даже в первом примере).

0

Вкратце, замыкания в JavaScript позволяют функции доступать к переменной, объявленной в лексально родительской функции.

Давайте рассмотрим более детальное объяснение. Чтобы понять замыкания, важно разобраться в том, как JavaScript обрабатывает область видимости переменных.

Области видимости

В JavaScript области видимости определяются функциями. Каждая функция создает новую область видимости.

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

function f() {
  // начало области видимости f
  var foo = 'hello'; // foo объявлена в области f
  for (var i = 0; i < 2; i++) { // i объявлена в области f
    // for-цикл не является функцией, поэтому мы все еще в области f
    var bar = 'Am I accessible?'; // bar объявлена в области f
    console.log(foo);
  }
  console.log(i);
  console.log(bar);
} // конец области видимости f

Вызов функции f выведет:

hello
hello
2
Am I Accessible?

Теперь рассмотрим случай, когда у нас есть функция g, определенная внутри другой функции f.

function f() {
  // начало области видимости f
  function g() {
    // начало области видимости g
    /*...*/
  } // конец области видимости g
  /*...*/
} // конец области видимости f

Мы будем называть f лексальным родителем для g. Как мы уже упоминали, теперь у нас есть две области видимости: f и g.

Но одна область "внутри" другой области, так что является ли область видимости дочерней функции частью области видимости родительской функции? Что происходит с переменными, объявленными в родительской функции? Сможем ли мы получить к ним доступ из дочерней функции? Здесь и вступают в силу замыкания.

Замыкания

В JavaScript функция g может не только получать доступ к любым переменным, объявленным в области g, но также и к любым переменным, объявленным в области родительской функции f.

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

function f() { // лексальная родительская функция
  // начало области видимости f
  var foo = 'hello'; // foo объявлена в области f
  function g() { // начало области видимости g
    var bar = 'bla'; // bar объявлена в области g
    console.log(foo);
  } // конец области видимости g
  g();
  console.log(bar);
} // конец области видимости f

Вызов функции f выведет:

hello
undefined

Посмотрим на строку console.log(foo);. В этот момент мы находимся в области видимости g и пытаемся получить доступ к переменной foo, объявленной в области видимости f. Как уже было сказано, мы можем получать доступ к любой переменной, объявленной в лексально родительской функции, что в данном случае и происходит; g является лексальным дочерним элементом f. Поэтому выводится hello.

Теперь обратим внимание на строку console.log(bar);. В этот момент мы находимся в области f и пытаемся получить доступ к переменной bar, объявленной в области g. Переменная bar не объявлена в текущей области, и функция g не является родителем f, поэтому bar будет undefined.

На самом деле мы также можем получить доступ к переменным, объявленным в области "дедушки" функции. Если бы была функция h, определенная внутри функции g, то h могла бы получить доступ ко всем переменным, объявленным в областях функций h, g и f. Это достигается с помощью замыканий. В JavaScript замыкания позволяют нам получать доступ к любым переменным, объявленным в лексальной родительской функции, лексальной "дедушке" функции и так далее, формируя таким образом цепочку областей видимости: область текущей функции -> область лексальной родительской функции -> область лексальной дедушки -> ... до последней функции-родителя, не имеющей родительской области.

Объект window

На самом деле цепочка не останавливается на последней родительской функции. Существует еще одна специальная область видимости — глобальная область. Каждая переменная, не объявленная в функции, считается объявленной в глобальной области. У глобальной области есть две особенности:

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

Следовательно, есть ровно два способа объявить переменную foo в глобальной области: либо не объявлять ее в функции, либо установить свойство foo объекта window.

Обе попытки используют замыкания

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

Создадим новый язык программирования: JavaScript-No-Closure. Как и следует из названия, JavaScript-No-Closure идентичен JavaScript, за исключением того, что не поддерживает замыкания.

Другими словами:

var foo = 'hello';
function f() { console.log(foo); }
f();
// JavaScript-No-Closure выведет undefined
// JavaScript выведет hello

Хорошо, давайте посмотрим, что произойдет с первым решением в JavaScript-No-Closure:

for (var i = 0; i < 10; i++) {
  (function() {
    var i2 = i;
    setTimeout(function() {
      console.log(i2); // i2 будет undefined в JavaScript-No-Closure
    }, 1000);
  })();
}

Таким образом, это выведет undefined 10 раз в JavaScript-No-Closure.

Теперь посмотрим на второе решение:

for (var i = 0; i < 10; i++) {
  setTimeout((function(i2) {
    return function() {
      console.log(i2); // i2 будет undefined в JavaScript-No-Closure
    }
  })(i), 1000);
}

Таким образом, это также выведет undefined 10 раз в JavaScript-No-Closure.

Оба решения используют замыкания.

P.S. Предполагается, что эти три фрагмента кода не определены в глобальной области видимости. В противном случае переменные foo и i были бы привязаны к объекту window и поэтому были бы доступны через объект window как в JavaScript, так и в JavaScript-No-Closure.

0

Проблема с объяснением замыканий в JavaScript заключается в том, что многие не понимают, как бы выглядел JS без замыканий.

Без замыканий это вызовет ошибку

function outerFunc(){
    var outerVar = 'переменная из outerFunc';
    return function(){
        alert(outerVar);
    }
}

outerFunc()(); // возвращает внутреннюю функцию и вызывает её

Когда outerFunc завершает выполнение в гипотетической версии JavaScript без замыканий, ссылка на outerVar была бы сборщиком мусора уничтожена, так что внутренней функции не осталось бы ничего, на что можно было бы ссылаться.

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

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

Но как только у вас появляется ссылка на внешнюю переменную в внутренней функции, это словно устанавливается дверной косяк, который препятствует сборке мусора для этих переменных.

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

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

function outerFunc(){
    var incrementMe = 0;
    return function(){ incrementMe++; console.log(incrementMe); }
}
var inc = outerFunc();
inc(); // выводит 1
inc(); // выводит 2
0

Закрытие (closure) — это особый объект, который объединяет в себе функцию и окружение, в котором эта функция была создана. Окружение состоит из любых локальных переменных, которые были доступны в момент создания закрытия.

В коде вашего друга функция function(){ console.log(i2); } определена внутри закрытия анонимной функции function(){ var i2 = i; ... } и может читать и изменять локальную переменную i2.

В вашем коде функция function(){ console.log(i2); } определяется внутри закрытия функции function(i2){ return ...} и также может читать и изменять локальную переменную i2 (в данном случае объявленную как параметр).

В обоих случаях функция function(){ console.log(i2); } передается в setTimeout.

Другой эквивалент (но с меньшим использованием памяти) выглядит так:

function fGenerator(i2) {
    return function() {
        console.log(i2);
    }
}
for (var i = 0; i < 10; i++) {
    setTimeout(fGenerator(i), 1000);
}

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

0

Давайте рассмотрим оба способа:

(function(){
    var i2 = i;
    setTimeout(function(){
        console.log(i2);
    }, 1000)
})();

Этот код объявляет и немедленно выполняет анонимную функцию, которая запускает setTimeout() в своем собственном контексте. Текущая величина i сохраняется путем создания копии в i2; это срабатывает благодаря немедленному выполнению функции.

setTimeout((function(i2){
    return function() {
        console.log(i2);
    }
})(i), 1000);

Здесь создается контекст исполнения для внутренней функции, в который сохраняется текущее значение i в i2; этот подход также использует немедленное выполнение для сохранения значения.

Важно

Стоит отметить, что семантика выполнения не одинакова для обоих подходов; ваша внутренняя функция передается в setTimeout(), тогда как его внутренняя функция сама вызывает setTimeout().

Оборачивание обоих кодов в другой setTimeout() не доказывает, что только второй подход использует замыкания; изначально это просто разные вещи.

Заключение

Оба метода используют замыкания, поэтому дело сводится к личным предпочтениям; второй подход проще "перемещать" или обобщать.

Чтобы ответить на вопрос, пожалуйста, войдите или зарегистрируйтесь