Как работают замыкания в JavaScript

124
05 ноября 2019, 06:30

Как сказал Альберт Эйнштейн:

Если вы не можете объяснить что-то шестилетнему, то вряд ли вы сами вполне понимаете.

Я тут попытался объяснить замыкания знакомому 27-ми лет, и он ничего не понял.

Как бы объяснить замыкания тому, кто знаком со всеми принципами программирования, необходимыми для понимания замыканий (функциями, переменными, областью видимости и т.д.), кроме, собственно, замыканий?

Этот вопрос переведён с англоязычного SO.

Answer 1

Это перевод из community wiki.

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

function внешняя(x) { 
  var tmp = 3; 
 
  function внутренняя(y) { 
    alert(x + y + (++tmp)); // выведет 16 
  } 
 
  внутренняя(10); 
} 
 
внешняя(2);

Этот код всегда выдаёт 16, потому, что функция внутренняя видит x, который является переменной в функуции внешняя. В данном случае аргументом функции. Так же внутренняя() может видить tmp из внешней().

Это и называется замыкание или closure. Если точнее, замыканием называется именно внешняя функция, а всё что внутри неё называется closure environment или среда замыкания.

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

function foo(x) { 
  var tmp = 3; 
 
  return function (y) { 
    alert(x + y + (++tmp)); // will also alert 16 
  } 
} 
 
var bar = foo(2); // bar is now a closure. 
bar(10);

Приведённая выше функция также выдаст 16, поскольку bar даже после завершения foo продолжает иметь доступ к x и tmp, пусть даже сама переменная bar и не находится внутри области видимости в которой они были объявлены.

При этом, поскольку переменная tmp всё ещё находится внутри замыкания bar, она продолжает увеличиваться при каждом вызове bar.

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

var a = 10;
function test() {
  console.log(a); // вывод 10
  console.log(b); // вывод 6
}
var b = 6;
test();

При запуске функции в JavaScript, для неё создаётся окружение, то есть список всех видимых ей переменных, не только аргументов и переменных объявленных внутри неё, но и снаружи, в данном примере это 'a' и 'b'.

Можно создать более чем одно замыкание в одном окружении, вернув их массивом, объектом или привязав к глобальным переменным. В таком случае, все они будут работать с тем же самым значением x или tmp, не создавая отдельных копий.

Поскольку в нашем примере x это число, то его значение копируется в foo как его аргумент x.

С другой стороны, в JavaScript всегда используются ссылки, когда передаются объекты. Если бы вы вызвали foo с объектом в качестве аргумента, то возвращённое замыкание вернуло бы ссылку на оригинальный объект!

function foo(x) { 
  var tmp = 3; 
 
  return function (y) { 
    alert(x + y + tmp); 
    x.memb = x.memb ? x.memb + 1 : 1; 
    alert(x.memb); 
  } 
} 
 
var age = new Number(2); 
var bar = foo(age); // bar теперь замыкание ссылающееся на age. 
bar(10);

Как и следовало ожидать, каждый вызов bar(10) увеличивает x.memb. Чего вы могли не ожидать, так это, что x продолжает ссылаться на тот же самый объект, что и age! После двух вызовов bar, age.memb будет равен 2! Кстати, так и происходят утечки памяти в HTML объектах.

Answer 2

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

Answer 3

Замыкание - это доступность переменных (области видимости) для текущей функции из вышестоящей функции, в которой она была объявлена

Answer 4

Это хороший вопрос!
Долго я сам не втыкал что к чему, хотя активно их использовал.
Со временем дошло (искренне на это надеюсь).
Но в двух словах тоже не раскажешь, вот хорошее описание их работы.
Итак:
Замыкания - это дальний родич инкапсуляции, наверное.
В JS область видимости создают только функции. Поэтому при создании любой функции у неё два особых свойства (их, конечно, больше, но мы их не рассматриваем): [[scope]] (там хранятся все созданные в ней сущности — переменные, агрументы, функции и пр., никак не доступна программисту) и __proto__ (ссылка на сущность, в которой создана функция, никак не должна быть доступна, можно влиять только через prototype, но некоторые браузеры успешно на это забивали).
Например, при запросе в функции переменной интерпретатор ищет её в соответствующем [[scope]], если не находит, то переходит по ссылке в __proto__ и обыскивает его скоуп. Не находит - опять переход и обыск. И только когда убеждается, что дошёл до конца (главный объект, window у браузеров), выдаёт окончательную ошибку что не нашёл переменную.
Т.к. внешнее окружение не имеет доступа до внутренних [[scope]] (оно о них даже не знает), то внешне невозможно добраться до внутренних переменных, когда внутренние легко добираются до внешних.

Если в двух словах и грубовато, то как-то так.

Answer 5
function mCounter(){
    var count = 0;
    function counter(){ // создаем функцию и увеличиваем переменную
        count = count + 1 ; // можно +=
        return count;
    }
    return counter; // возрашаем функцию которая использует
 свободную переменную. Это замыкание.
}
var dCount = mCounter();//здесь привызове mCounter() мы получаем замыкание(функцию с окружением)
console.log(dCount());//1  при вызове dCount()
//  значение count берется из переменной count,
// которая находится в окружении замыкания.
console.log(dCount());//2
console.log(dCount());//3
console.log(dCount());//4
//мы вызываем функцию которая создает
// др.функцию и возвращает ее вместе с окружением,
// в нашем случае содержащим свободную переменную count.
// Это и есть замыкание.
//свободная переменная count не связана ни с какими значениями
(потому и наз.свободная)
//т.е. count объявляется за пределами функции counter()
//в окружении, где наша свободная переменная , функция 
становится замкнутой, а
//если взять функцию с ее окружением, получится замыкание.
Answer 6

объяснить на примере.

function func1() {
     var a = Math.random();
     return function () {
            return a;
     }
}
var f = func1();
console.log(f());
var f2 = func1();
console.log(f2());
0.34218455478549004
0.6286177076399326
Answer 7

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

function sum(a) { 
  const add = function(b) { 
    if (b !== undefined) { 
      a = a + b; 
      return add; 
    } 
    return a; 
  } 
  return add; 
} 
 
console.log(sum(1)(2)(3)(4)()) //10

READ ALSO
Как сделать функцию на два элемента?

Как сделать функцию на два элемента?

написала скрипт эмуляции проверкиНа первом элемента все работает, на втором - нет

128
Пагинация и чек боксы! Как их подружить? ASP.NET Core

Пагинация и чек боксы! Как их подружить? ASP.NET Core

Код, который получает отмеченные чек боксы и передаёт их методу в контроллер

101
Как удалить значение из input?

Как удалить значение из input?

Есть такая задача: Есть модальное окно, которое открывается при клике на InputВ этом модальном юзер выбирает определенные категории

214
Выпадающее меню на всю высоту

Выпадающее меню на всю высоту

Хочу сделать выпадающее меню на всю высоту экранаСделал через Height: 100vh

148