Как работают закрытия JavaScript?

7636

Как бы вы объяснили JavaScript-замыкания кому-то, кто знает концепции, из которых они состоят (например, функции, переменные и т. П.), Но не понимает сами замыкания?

Я видел пример схемы, приведенный в Википедии, но, к сожалению, это не помогло.

Zaheer Ahmed
источник
391
Моя проблема с этими и многими ответами состоит в том, что они подходят к нему с абстрактной, теоретической точки зрения, а не начинают просто объяснять, почему замыкания необходимы в Javascript и в практических ситуациях, в которых вы их используете. В итоге вы получаете статью, которую вы должны пролистать, все время думая: «Но почему?». Я бы просто начал с: замыкания - отличный способ справиться со следующими двумя реалиями JavaScript: a. область видимости находится на уровне функций, а не на уровне блоков и, б. большая часть того, что вы делаете на практике в JavaScript, является асинхронной / управляемой событиями.
Джереми Бертон,
53
@Redsandro С одной стороны, это значительно облегчает написание кода, управляемого событиями. Я мог бы запустить функцию при загрузке страницы, чтобы определить особенности HTML или доступных функций. Я могу определить и установить обработчик в этой функции и иметь всю эту контекстную информацию доступной каждый раз, когда обработчик вызывается без необходимости повторного запроса. Решите проблему один раз, повторно используйте ее на каждой странице, где требуется этот обработчик, с уменьшенными накладными расходами при повторном вызове обработчика. Вы когда-нибудь видели, чтобы одни и те же данные дважды отображались на языке, в котором их нет? Закрытия делают намного проще избежать подобных вещей.
Эрик Реппен
1
@Erik Reppen спасибо за ответ. На самом деле, мне было любопытно узнать о преимуществах этого трудно читаемого closureкода, в отличие от того, Object Literalкоторый повторно использует себя и точно так же снижает накладные расходы, но требует на 100% меньше кода для переноса.
Redsandro
6
Для программистов на Java короткий ответ - это функция, эквивалентная внутреннему классу. Внутренний класс также содержит неявный указатель на экземпляр внешнего класса и используется для почти той же цели (то есть для создания обработчиков событий).
Борис ван Скотэн
8
Я нашел этот практический пример очень полезным: youtube.com/watch?v=w1s9PgtEoJs
Abhi

Ответы:

7360

Закрытие представляет собой сочетание:

  1. Функция и
  2. Ссылка на внешнюю область действия этой функции (лексическая среда)

Лексическая среда является частью каждого контекста выполнения (стекового фрейма) и представляет собой карту между идентификаторами (то есть именами локальных переменных) и значениями.

Каждая функция в JavaScript поддерживает ссылку на внешнюю лексическую среду. Эта ссылка используется для настройки контекста выполнения, созданного при вызове функции. Эта ссылка позволяет коду внутри функции «видеть» переменные, объявленные вне функции, независимо от того, когда и где вызывается функция.

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

В следующем коде innerсоздается закрытие с лексической средой контекста выполнения, созданной при fooвызове, закрывающей переменную secret:

function foo() {
  const secret = Math.trunc(Math.random()*100)
  return function inner() {
    console.log(`The secret number is ${secret}.`)
  }
}
const f = foo() // `secret` is not directly accessible from outside `foo`
f() // The only way to retrieve `secret`, is to invoke `f`

Другими словами: в JavaScript функции содержат ссылку на частный «блок состояния», к которому имеют доступ только они (и любые другие функции, объявленные в той же лексической среде). Этот блок состояния невидим для вызывающей функции, предоставляя отличный механизм для сокрытия данных и инкапсуляции.

И помните: функции в JavaScript могут передаваться как переменные (функции первого класса), что означает, что эти пары функциональности и состояния могут передаваться по вашей программе: подобно тому, как вы можете передавать экземпляр класса в C ++.

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

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

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

В C и большинстве других распространенных языков после возврата функции все локальные переменные больше не доступны, потому что стековый фрейм разрушен. В JavaScript, если вы объявляете функцию в другой функции, то локальные переменные внешней функции могут оставаться доступными после возврата из нее. Таким образом, в приведенном выше коде secretостается доступным для объекта функции после того inner, как он был возвращен из foo.

Использование Закрытий

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

Переменные частного экземпляра

В следующем коде функция toStringзакрывает детали автомобиля.

function Car(manufacturer, model, year, color) {
  return {
    toString() {
      return `${manufacturer} ${model} (${year}, ${color})`
    }
  }
}
const car = new Car('Aston Martin','V8 Vantage','2012','Quantum Silver')
console.log(car.toString())

Функциональное программирование

В следующем коде функция innerзакрывается как fnи args.

function curry(fn) {
  const args = []
  return function inner(arg) {
    if(args.length === fn.length) return fn(...args)
    args.push(arg)
    return inner
  }
}

function add(a, b) {
  return a + b
}

const curriedAdd = curry(add)
console.log(curriedAdd(2)(3)()) // 5

Событийно-ориентированное программирование

В следующем коде функция onClickзакрывается над переменной BACKGROUND_COLOR.

const $ = document.querySelector.bind(document)
const BACKGROUND_COLOR = 'rgba(200,200,242,1)'

function onClick() {
  $('body').style.background = BACKGROUND_COLOR
}

$('button').addEventListener('click', onClick)
<button>Set background color</button>

Модульность

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

let namespace = {};

(function foo(n) {
  let numbers = []
  function format(n) {
    return Math.trunc(n)
  }
  function tick() {
    numbers.push(Math.random() * 100)
  }
  function toString() {
    return numbers.map(format)
  }
  n.counter = {
    tick,
    toString
  }
}(namespace))

const counter = namespace.counter
counter.tick()
counter.tick()
console.log(counter.toString())

Примеры

Пример 1

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

function foo() {
  let x = 42
  let inner  = function() { console.log(x) }
  x = x+1
  return inner
}
var f = foo()
f() // logs 43

Пример 2

В следующем коде, тремя способами log, incrementи updateвсе близкие по одной и той же лексической среде.

И каждый раз, когда createObjectвызывается, создается новый контекст выполнения (стековый фрейм) и создается совершенно новая переменная x, а также новый набор функций ( logи т. Д.), Которые закрываются над этой новой переменной.

function createObject() {
  let x = 42;
  return {
    log() { console.log(x) },
    increment() { x++ },
    update(value) { x = value }
  }
}

const o = createObject()
o.increment()
o.log() // 43
o.update(5)
o.log() // 5
const p = createObject()
p.log() // 42

Пример 3

Если вы используете переменные, объявленные с использованием var, будьте осторожны, понимая, какую переменную вы закрываете. Переменные, объявленные с использованием var, поднимаются. Это намного меньше проблем в современном JavaScript из-за введения letи const.

В следующем коде каждый раз вокруг цикла создается новая функция inner, которая закрывается i. Но поскольку он var iнаходится вне цикла, все эти внутренние функции закрываются по одной и той же переменной, а это означает, что окончательное значение i(3) печатается три раза.

function foo() {
  var result = []
  for (var i = 0; i < 3; i++) {
    result.push(function inner() { console.log(i) } )
  }
  return result
}

const result = foo()
// The following will print `3`, three times...
for (var i = 0; i < 3; i++) {
  result[i]() 
}

Финальные очки:

  • Всякий раз, когда функция объявляется в JavaScript, создается замыкание.
  • Возвращение function изнутри другой функции является классическим примером замыкания, поскольку состояние внутри внешней функции неявно доступно для возвращенной внутренней функции даже после того, как внешняя функция завершила выполнение.
  • Всякий раз, когда вы используете eval()внутри функции, используется замыкание. В тексте вы evalможете ссылаться на локальные переменные функции, а в нестрогом режиме вы даже можете создавать новые локальные переменные, используяeval('var foo = …') .
  • Когда вы используете new Function(…)( конструктор Function ) внутри функции, она не закрывается в своей лексической среде: вместо этого она закрывается в глобальном контексте. Новая функция не может ссылаться на локальные переменные внешней функции.
  • Закрытие в JavaScript похоже на сохранение ссылки ( НЕ копии) на область действия в точке объявления функции, которая, в свою очередь, сохраняет ссылку на ее внешнюю область и т. Д. Вплоть до глобального объекта в верхней части цепь прицела.
  • Закрытие создается, когда функция объявлена; это закрытие используется для настройки контекста выполнения при вызове функции.
  • Новый набор локальных переменных создается каждый раз, когда вызывается функция.

связи

Ben Aston
источник
74
Это звучит хорошо: «Закрытие в JavaScript похоже на сохранение копии всех локальных переменных, как это было при выходе из функции». Но это вводит в заблуждение по нескольким причинам. (1) Вызов функции не должен выходить, чтобы создать замыкание. (2) Это не копия значений локальных переменных, а сами переменные. (3) Там не сказано, кто имеет доступ к этим переменным.
dlaliberte
27
В примере 5 показана «ошибка», когда код работает не так, как задумано. Но это не показывает, как это исправить. Этот другой ответ показывает способ сделать это.
Мэтт
190
Мне нравится, как этот пост начинается большими жирными буквами с надписью «Закрытия не являются магическими» и заканчивается первым примером: «Волшебство состоит в том, что в JavaScript ссылка на функцию также имеет секретную ссылку на замыкание, в котором она была создана».
Эндрю Мачерет
6
Пример №3 - смешивание замыканий с подъемом javascript. Теперь я думаю, что объяснить только замыкания достаточно сложно, не привлекая подъемного поведения. Это помогло мне больше всего: Closures are functions that refer to independent (free) variables. In other words, the function defined in the closure 'remembers' the environment in which it was created.от developer.mozilla.org/en-US/docs/Web/JavaScript/Closures
caramba
3
ECMAScript 6 может изменить что-то в этой замечательной статье о закрытии. Например, если вы используете let i = 0вместо var i = 0примера 5, он testList()напечатает то, что вы хотели изначально.
Nier
3989

Каждая функция в JavaScript поддерживает ссылку на внешнюю лексическую среду. Лексическая среда - это карта всех имен (например, переменных, параметров) в области видимости с их значениями.

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

function foo(x) {
  var tmp = 3;

  function bar(y) {
    console.log(x + y + (++tmp)); // will log 16
  }

  bar(10);
}

foo(2);

Это будет регистрироваться, 16потому что функция barзакрывает параметр xи переменную tmp, которые существуют в лексическом окружении внешней функции.foo .

Функция barвместе с ее связью с лексическим окружением функции fooявляется замыканием.

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

function foo(x) {
  var tmp = 3;

  return function (y) {
    console.log(x + y + (++tmp)); // will also log 16
  }
}

var bar = foo(2);
bar(10); // 16
bar(10); // 17

Вышеприведенная функция также зарегистрирует 16, потому что код внутри barвсе еще может ссылаться на аргумент xи переменнуюtmp , даже если они больше не находятся в области видимости.

Тем не менее, так tmpкак все еще находится внутри barзамыкания, его можно увеличивать. Он будет увеличиваться каждый раз, когда вы звонитеbar .

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

var a = 10;

function test() {
  console.log(a); // will output 10
  console.log(b); // will output 6
}
var b = 6;
test();

Когда вызывается функция JavaScript, создается новый контекст выполнения ec. Вместе с аргументами функции и целевым объектом этот контекст выполнения также получает ссылку на лексическую среду вызывающего контекста выполнения, то есть переменные, объявленные во внешней лексической среде (в вышеприведенном примере оба aи b) доступны из ec.

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

Обратите внимание, что сами переменные видны из замыкания, а не из копий.

Али
источник
24
@feeela: Да, каждая функция JS создает замыкание. Переменные, на которые нет ссылок, вероятно, будут иметь право на сборку мусора в современных механизмах JS, но это не меняет того факта, что при создании контекста выполнения этот контекст имеет ссылку на включающий контекст выполнения и его переменные, и эта функция является объектом, который потенциально может быть перемещен в другую переменную область, сохраняя при этом эту исходную ссылку. Это закрытие.
@ Али Я только что обнаружил, что jsFiddle, который я предоставил, на самом деле ничего не доказывает, так как deleteтерпит неудачу. Тем не менее, лексическая среда, которую функция будет переносить как [[Scope]] (и, в конечном счете, использовать в качестве основы для своей собственной лексической среды при вызове), определяется при выполнении оператора, определяющего функцию. Это означает , что функция является закрытие над содержимым ЦЕЛЫХ исполняющего объема, независимо от того, какие значения он на самом деле относится к и избегает сферы ли. Пожалуйста, посмотрите разделы 13.2 и 10 в спецификации
Асад Саидуддин
8
Это был хороший ответ, пока он не попытался объяснить примитивные типы и ссылки. Это совершенно неправильно и говорит о копируемых литералах, которые на самом деле не имеют ничего общего.
Ry-
12
Замыкания являются ответом JavaScript на объектно-ориентированное программирование на основе классов. JS не основан на классах, поэтому нужно было найти другой способ реализации некоторых вещей, которые не могли бы быть реализованы иначе.
Бартломей Залевский
2
это должен быть принятый ответ. Волшебство никогда не происходит во внутренней функции. Это происходит при назначении внешней функции переменной. Это создает новый контекст выполнения для внутренней функции, так что «приватная переменная» может накапливаться. Конечно, это возможно, поскольку переменная, которой назначена внешняя функция, поддерживает контекст. Первый ответ просто усложнит ситуацию, не объясняя, что на самом деле там происходит.
Альберт Гао
2442

ПРЕДИСЛОВИЕ: этот ответ был написан, когда вопрос был:

Как сказал старый Альберт: «Если вы не можете объяснить это шестилетнему, вы действительно сами этого не понимаете». Ну, я попытался объяснить закрытие JS другу 27 лет и потерпел неудачу.

Кто-нибудь может считать, что мне 6 лет и странно интересуюсь этой темой?

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


Я большой поклонник аналогий и метафор при объяснении сложных концепций, поэтому позвольте мне попробовать свои силы с историей.

Давным-давно:

Там была принцесса ...

function princess() {

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

    var adventures = [];

    function princeCharming() { /* ... */ }

    var unicorn = { /* ... */ },
        dragons = [ /* ... */ ],
        squirrel = "Hello!";

    /* ... */

Но ей всегда придется возвращаться в свой скучный мир дел и взрослых.

    return {

И она часто рассказывала им о своем последнем удивительном приключении в роли принцессы.

        story: function() {
            return adventures[adventures.length - 1];
        }
    };
}

Но все, что они увидят, это маленькая девочка ...

var littleGirl = princess();

... рассказывать истории о магии и фантазии.

littleGirl.story();

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

Но мы знаем настоящую правду; что маленькая девочка с принцессой внутри ...

... действительно принцесса с маленькой девочкой внутри.

Jacob Swartwood
источник
340
Мне действительно нравится это объяснение. Для тех, кто читает и не следует, аналогия такова: функция princess () представляет собой сложную область, содержащую личные данные. За пределами функции личные данные не могут быть видны или доступны. Принцесса держит в своем воображении единорогов, драконов, приключений и т. Д. (Личные данные), и взрослые не могут увидеть их сами. НО воображение принцессы захвачено в закрытии для story()функции, которая является единственным интерфейсом, который littleGirlэкземпляр выставляет в мир магии.
Патрик М
Итак, вот storyзакрытие, но если бы код был var story = function() {}; return story;тогда, littleGirlбыло бы закрытие. По крайней мере, такое впечатление, которое я получаю от использования MDN «закрытых» методов с замыканиями : «Эти три публичные функции являются замыканиями, которые используют одну и ту же среду».
icc97
16
@ icc97, да, storyэто замыкание, ссылающееся на среду, предоставляемую в рамках princess. princessтакже является другим подразумеваемым замыканием, т. е. princessи littleGirlразделяют любую ссылку на parentsмассив, который будет существовать назад в среде / области действия, где littleGirlсуществует и princessопределено.
Джейкоб Свартвуд
6
@BenjaminKrupp Я добавил явный комментарий к коду, чтобы показать / подразумевать, что в теле больше операций, princessчем написано. К сожалению, эта история сейчас немного неуместна в этой теме. Первоначально вопрос был о том, чтобы «объяснить закрытие JavaScript 5-летнему»; мой ответ был единственным, который даже пытался это сделать. Я не сомневаюсь, что это с треском провалилось бы, но, по крайней мере, у этого ответа мог быть шанс заинтересовать пятилетнего ребенка.
Джейкоб Свартвуд
11
На самом деле, для меня это имело смысл. И я должен признать, что, наконец, понимание закрытия JS, используя рассказы о принцессах и приключениях, заставляет меня чувствовать себя немного странно.
Кристаллизация
753

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

О развитии детства: от 5 до 7 лет говорится:

Ваш ребенок сможет следовать двухшаговым инструкциям. Например, если вы скажете своему ребенку: «Иди на кухню и принеси мне мешок для мусора», они смогут запомнить это направление.

Мы можем использовать этот пример для объяснения замыканий следующим образом:

Кухня - это закрытие, которое имеет локальную переменную, называемую trashBags. На кухне есть функция, которая вызывает getTrashBagодин мешок для мусора и возвращает его.

Мы можем закодировать это в JavaScript следующим образом:

function makeKitchen() {
  var trashBags = ['A', 'B', 'C']; // only 3 at first

  return {
    getTrashBag: function() {
      return trashBags.pop();
    }
  };
}

var kitchen = makeKitchen();

console.log(kitchen.getTrashBag()); // returns trash bag C
console.log(kitchen.getTrashBag()); // returns trash bag B
console.log(kitchen.getTrashBag()); // returns trash bag A

Другие пункты, которые объясняют, почему замыкания интересны:

  • Каждый раз, когда makeKitchen()вызывается, создается новое замыкание со своим отдельнымtrashBags .
  • trashBagsПеременная локальна внутри каждой кухне и не доступны снаружи, а внутренняя функция наgetTrashBag имущество имеет к нему доступ.
  • Каждый вызов функции создает замыкание, но не было бы необходимости хранить замыкание вокруг, если только внутренняя функция, имеющая доступ к внутренней части замыкания, не может быть вызвана извне замыкания. Возврат объекта с помощью getTrashBagфункции делает это здесь.
dlaliberte
источник
6
На самом деле, смешение, функция makeKitchen вызов является фактическим закрытием, а не объект кухни , что она возвращается.
dlaliberte
6
Пробираясь через других, я нашел этот ответ как самый простой способ объяснить, что и почему закрывает .is.
Четабахана
3
Слишком много меню и закусок, не хватает мяса и картошки. Вы можете улучшить этот ответ одним коротким предложением, например: «Закрытие - это запечатанный контекст функции из-за отсутствия какого-либо механизма определения объема, предоставляемого классами».
Степлерфахрер
584

Соломенный Человек

Мне нужно знать, сколько раз была нажата кнопка, и что-то делать при каждом третьем нажатии ...

Достаточно очевидное решение

// Declare counter outside event handler's scope
var counter = 0;
var element = document.getElementById('button');

element.addEventListener("click", function() {
  // Increment outside counter
  counter++;

  if (counter === 3) {
    // Do something every third time
    console.log("Third time's the charm!");

    // Reset counter
    counter = 0;
  }
});
<button id="button">Click Me!</button>

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

Рассмотрим этот вариант

var element = document.getElementById('button');

element.addEventListener("click", (function() {
  // init the count to 0
  var count = 0;

  return function(e) { // <- This function becomes the click handler
    count++; //    and will retain access to the above `count`

    if (count === 3) {
      // Do something every third time
      console.log("Third time's the charm!");

      //Reset counter
      count = 0;
    }
  };
})());
<button id="button">Click Me!</button>

Обратите внимание на несколько вещей здесь.

В приведенном выше примере я использую поведение закрытия JavaScript. Такое поведение позволяет любой функции иметь доступ к области, в которой она была создана, на неопределенный срок. Чтобы применить это на практике, я немедленно вызываю функцию, которая возвращает другую функцию, и поскольку функция, которую я возвращаю, имеет доступ к внутренней переменной count (из-за описанного выше поведения замыкания), это приводит к закрытой области видимости для использования в результате функция ... не так просто? Давай разбавим это ...

Простое однострочное закрытие

//          _______________________Immediately invoked______________________
//         |                                                                |
//         |        Scope retained for use      ___Returned as the____      |
//         |       only by returned function   |    value of func     |     |
//         |             |            |        |                      |     |
//         v             v            v        v                      v     v
var func = (function() { var a = 'val'; return function() { alert(a); }; })();

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

func();  // Alerts "val"
func.a;  // Undefined

Возьми? Таким образом, в нашем основном примере переменная count содержится в замыкании и всегда доступна обработчику событий, поэтому она сохраняет свое состояние от щелчка к щелчку.

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

Вот, пожалуйста. теперь вы полностью инкапсулируете это поведение.

Полный пост в блоге (включая вопросы jQuery)

jondavidjohn
источник
11
Я не согласен с вашим определением, что такое закрытие. Нет причин, по которым он должен вызывать себя. Также немного упрощенно (и неточно) говорить, что его нужно «вернуть» (об этом много говорится в комментариях к лучшему ответу на этот вопрос)
Джеймс Монтань,
40
@ Джеймс, даже если вы не согласны, его пример (и весь пост) - один из лучших, которые я видел. Хотя вопрос не старый и не решен для меня, он вполне заслуживает +1.
Э-Удовлетворение
84
«Мне нужно знать, сколько раз была нажата кнопка, и делать что-то при каждом третьем щелчке ...» ЭТО привлек мое внимание. Вариант использования и решение, показывающее, как закрытие не такая загадочная вещь, и что многие из нас пишут их, но точно не знают официального названия.
Chris22
Хороший пример, потому что он показывает, что «count» во 2-м примере сохраняет значение «count» и не сбрасывается в 0 при каждом нажатии «элемента». Очень информативно!
Адам
+1 за закрытие поведения . Можно ли ограничить поведение закрытия для функций в JavaScript или эта концепция может быть также применена к другим структурам языка?
Дзямид
493

Замыкания трудно объяснить, потому что они используются для того, чтобы заставить поведение работать так, как все интуитивно ожидают, что оно все равно будет работать. Я нахожу лучший способ объяснить их (и способ, которым я узнал, что они делают), это представить ситуацию без них:

    var bind = function(x) {
        return function(y) { return x + y; };
    }
    
    var plus5 = bind(5);
    console.log(plus5(3));

Что бы произошло здесь, если бы JavaScript не знал замыканий? Просто замените вызов в последней строке на тело метода (что в основном и делают вызовы функций), и вы получите:

console.log(x + 3);

Теперь, где определение x? Мы не определили это в текущем объеме. Единственное решение состоит в том, чтобы позволить plus5 нести его область (или, скорее, область его родителя) вокруг. Этот способ xчетко определен и связан со значением 5.

Конрад Рудольф
источник
11
Это именно тот вид , например , что вводит в заблуждение многих людей, думая , что это ценности , которые используются в возвращаемой функции, а не сама изменяемая переменная. Если бы он был изменен на «return x + = y», или, что еще лучше, и ту, и другую функцию «x * = y», то было бы ясно, что ничего не копируется. Если вы привыкли складывать кадры, представьте себе, что вместо этого используйте кадры кучи, которые могут продолжать существовать после возврата из функции.
Мэтт
14
@ Мэтт, я не согласен. Пример не должен исчерпывающе документировать все свойства. Он предназначен для упрощения и иллюстрирует существенную особенность концепции. ФП попросил простое объяснение («для шестилетнего ребенка»). Возьмите принятый ответ: он не может дать краткого объяснения именно потому, что пытается быть исчерпывающим. (Я согласен с вами, что важным свойством JavaScript является то, что привязка осуществляется по ссылке, а не по значению… но, опять же, успешное объяснение - это то, которое сводится к минимуму.)
Конрад Рудольф
@KonradRudolph Мне нравится стиль и краткость вашего примера. Я просто рекомендую немного изменить его, чтобы заключительная часть, «Единственное решение ...», стала правдой. В настоящее время существует на самом деле другое, более простое решение для вашего сценария, который не не соответствует яваскрипту продолжений, и делает соответствуют распространенному заблуждению о том , что продолжения есть. Таким образом, пример в его нынешнем виде опасен. Это не связано с исчерпывающим списком свойств, оно связано с пониманием значения x в возвращаемой функции, что, в конце концов, является основным пунктом.
Мэтт
@Matt Хм, я не уверен, что полностью вас понимаю, но начинаю понимать, что у вас может быть правильное мнение. Так как комментарии слишком короткие, не могли бы вы объяснить, что вы имеете в виду в gist / pastie или в чате? Спасибо.
Конрад Рудольф
2
@KonradRudolph Я думаю, что я не был ясно о цели х + = у. Цель состояла в том, чтобы просто показать, что повторяющиеся вызовы возвращаемой функции продолжают использовать одну и ту же переменную x (в отличие от одного и того же значения , которое, как думают люди, «вставляется» при создании функции). Это похоже на первые два оповещения в вашей скрипке. Целью дополнительной функции x * = y было бы показать, что все возвращаемые функции имеют один и тот же x.
Мэтт
379

TLDR

Замыкание - это связь между функцией и ее внешней лексической (то есть, как написано) средой, так что идентификаторы (переменные, параметры, объявления функций и т. Д.), Определенные в этой среде, видны изнутри функции, независимо от того, когда или из где функция вызывается.

подробности

В терминологии спецификации ECMAScript можно сказать, что замыкание реализовано посредством [[Environment]]ссылки на каждый объект-функцию, которая указывает на лексическую среду, в которой определена функция.

Когда функция вызывается с помощью внутреннего [[Call]]метода, то [[Environment]]ссылка на функцию-объект копируется в внешней среды ссылкой на записи среды вновь созданного контекста выполнения (кадра стека).

В следующем примере функция fзакрывается над лексической средой глобального контекста выполнения:

function f() {}

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

function g() {
    function h() {}
}

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

В следующем примере функция jзакрывается над лексическим окружением функции i, что означает, что переменная xвидна изнутри функции j, еще долго после того, как функция iзавершила выполнение:

function i() {
    var x = 'mochacchino'
    return function j() {
        console.log('Printing the value of x, from within function j: ', x)
    }
} 

const k = i()
setTimeout(k, 500) // invoke k (which is j) after 500ms

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

function l() {
  var y = 'vanilla';

  return {
    setY: function(value) {
      y = value;
    },
    logY: function(value) {
      console.log('The value of y is: ', y);
    }
  }
}

const o = l()
o.logY() // The value of y is: vanilla
o.setY('chocolate')
o.logY() // The value of y is: chocolate

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

Обратите внимание, что в попытке улучшить ясность и точность, этот ответ был существенно изменен по сравнению с оригиналом.

Ben
источник
56
Ничего себе, никогда не знал, что вы можете использовать строковые замены console.logтаким образом. Если кому-то еще интересно, есть еще: developer.mozilla.org/en-US/docs/DOM/…
Flash
7
Переменные, которые находятся в списке параметров функции, также являются частью замыкания (например, не только var).
Томас Эдинг
Замыкания звучат больше как объекты, классы и т. Д. Не уверен, почему многие не сравнивают эти два - нам, новичкам, будет легче учиться!
Альмаруф
377

Хорошо, 6-летний поклонник закрытий. Хотите услышать самый простой пример закрытия?

Давайте представим следующую ситуацию: водитель сидит в машине. Эта машина в самолете. Самолет в аэропорту. Возможность водителя получить доступ к вещам вне его автомобиля, но внутри самолета, даже если этот самолет покидает аэропорт, является закрытием. Вот и все. Когда вам исполнится 27 лет, посмотрите на более подробное объяснение или на пример ниже.

Вот как я могу преобразовать свою историю самолета в код.

var plane = function(defaultAirport) {

  var lastAirportLeft = defaultAirport;

  var car = {
    driver: {
      startAccessPlaneInfo: function() {
        setInterval(function() {
          console.log("Last airport was " + lastAirportLeft);
        }, 2000);
      }
    }
  };
  car.driver.startAccessPlaneInfo();

  return {
    leaveTheAirport: function(airPortName) {
      lastAirportLeft = airPortName;
    }
  }
}("Boryspil International Airport");

plane.leaveTheAirport("John F. Kennedy");

Макс Ткаченко
источник
26
Хорошо сыграно и отвечает оригинальному постеру. Я думаю, что это лучший ответ. Я собирался использовать багаж таким же образом: представьте, что вы идете в дом бабушки, и вы упаковываете свой футляр Nintendo DS с игровыми картами внутри своего футляра, но затем кладете футляр в свой рюкзак и кладете игровые карты в карманы рюкзака, и Затем вы положили все это в большой чемодан с большим количеством игровых карт в карманах чемодана. Добравшись до дома бабушки, вы можете играть в любую игру на своем DS, если все внешние случаи открыты. Или что-то в этом роде.
Слартибартфаст
366

Это попытка прояснить несколько (возможных) недоразумений о замыканиях, которые появляются в некоторых других ответах.

  • Закрытие создается не только когда вы возвращаете внутреннюю функцию. Фактически, функция включения вообще не должна возвращаться для создания своего замыкания. Вместо этого вы можете назначить свою внутреннюю функцию переменной во внешней области видимости или передать ее в качестве аргумента другой функции, где она может быть вызвана немедленно или в любое время позже. Следовательно, замыкание закрывающей функции, вероятно, создается, как только вызывается охватывающая функция, поскольку любая внутренняя функция имеет доступ к этому замыканию всякий раз, когда вызывается внутренняя функция, до или после возврата закрывающей функции.
  • Закрытие не ссылается на копию старых значений переменных в своей области видимости. Сами переменные являются частью замыкания, и поэтому значение, видимое при обращении к одной из этих переменных, является самым последним значением на момент обращения к нему. Вот почему внутренние функции, созданные внутри циклов, могут быть хитрыми, поскольку каждая из них имеет доступ к одним и тем же внешним переменным, а не захватывает копию переменных во время создания или вызова функции.
  • «Переменные» в замыкании включают любые именованные функции, объявленные внутри функции. Они также включают аргументы функции. Закрытие также имеет доступ к переменным, содержащим закрытие, вплоть до глобальной области видимости.
  • Замыкания используют память, но они не вызывают утечек памяти, поскольку JavaScript сам очищает свои собственные циклические структуры, на которые нет ссылок. Утечки памяти в Internet Explorer, связанные с замыканиями, создаются, когда ему не удается отключить значения атрибута DOM, которые ссылаются на замыкания, таким образом сохраняя ссылки на, возможно, циклические структуры.
dlaliberte
источник
15
Джеймс, я сказал, что замыкание «вероятно» создается во время вызова включающей функции, потому что вполне вероятно, что реализация может отложить создание замыкания до некоторого времени спустя, когда решит, что замыкание абсолютно необходимо. Если в функции включения не определена внутренняя функция, закрытие не требуется. Поэтому, возможно, он может подождать, пока будет создана первая внутренняя функция, а затем создать замыкание из контекста вызова включающей функции.
dlaliberte
9
@ Beetroot-Beetroot Предположим, у нас есть внутренняя функция, которая передается другой функции, где она используется до возврата внешней функции, и предположим, что мы также возвращаем ту же внутреннюю функцию из внешней функции. В обоих случаях это одна и та же функция, но вы говорите, что перед возвратом внешней функции внутренняя функция «привязывается» к стеку вызовов, тогда как после возврата внутренняя функция внезапно связывается с замыканием. Он ведет себя одинаково в обоих случаях; семантика идентична, поэтому вы не просто говорите о деталях реализации?
dlaliberte
7
@ Beetroot-Beetroot, спасибо за ваш отзыв, и я рад, что вы подумали. Я до сих пор не вижу какой-либо семантической разницы между живым контекстом внешней функции и тем же контекстом, когда она становится замыканием, когда функция возвращается (если я понимаю ваше определение). Внутренняя функция не волнует. Сборку мусора не волнует, поскольку внутренняя функция в любом случае поддерживает ссылку на контекст / замыкание, а вызывающая сторона внешней функции просто отбрасывает ссылку на контекст вызова. Но это сбивает с толку людей, и, возможно, лучше просто назвать это контекстом вызова.
dlaliberte
9
Эту статью трудно читать, но я думаю, что она действительно поддерживает то, что я говорю. В нем говорится: «Закрытие формируется путем возврата объекта функции [...] или путем непосредственного присвоения ссылки на такой объект функции, например, глобальной переменной». Я не имею в виду, что GC не имеет значения. Скорее, из-за GC и потому, что внутренняя функция присоединена к контексту вызова внешней функции (или [[scope]], как говорится в статье), тогда не имеет значения, возвращается ли вызов внешней функции, потому что это связывание с внутренним функция это важная вещь.
Длалиберте
3
Отличный ответ! Следует добавить, что все функции закрываются по всему содержимому исполняемой области, в которой они определены. Не имеет значения, ссылаются ли они на некоторые или ни на одну из переменных из родительской области: ссылка на лексическое окружение родительской области безоговорочно сохраняется как [[Scope]]. Это видно из раздела о создании функций в спецификации ECMA.
Асад Саидуддин
236

Некоторое время назад я написал сообщение в блоге, объясняющее закрытие. Вот то, что я сказал о замыканиях с точки зрения того, почему вы хотите его.

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

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

Полный пост:

Так что же это за штуковины?

Натан Лонг
источник
Так можно ли было бы подчеркнуть основное преимущество замыканий на этом примере? Скажем, у меня есть функция emailError (sendToAddress, errorString), которую я мог бы затем сказать, devError = emailError("devinrhode2@googmail.com", errorString)а затем иметь свою собственную версию общей функции emailError?
Девин Г Род
Это объяснение и связанный с ним прекрасный пример в ссылке на (closure thingys) - лучший способ понять замыкания и должны быть прямо вверху!
HopeKing
215

Закрытия просты:

Следующий простой пример охватывает все основные моменты замыканий JavaScript. *  

Вот фабрика, которая производит калькуляторы, которые можно добавлять и умножать:

function make_calculator() {
  var n = 0; // this calculator stores a single number n
  return {
    add: function(a) {
      n += a;
      return n;
    },
    multiply: function(a) {
      n *= a;
      return n;
    }
  };
}

first_calculator = make_calculator();
second_calculator = make_calculator();

first_calculator.add(3); // returns 3
second_calculator.add(400); // returns 400

first_calculator.multiply(11); // returns 33
second_calculator.multiply(10); // returns 4000

Ключевой момент: при каждом вызове make_calculatorсоздается новая локальная переменная n, которая продолжает использоваться калькулятором addи multiplyфункциями еще долго после make_calculatorвозврата.

Если вы знакомы со стековыми фреймами, эти калькуляторы кажутся странными: как они могут получить доступ nпосле make_calculatorвозврата? Ответ заключается в том, чтобы представить, что JavaScript не использует «стековые фреймы», а вместо этого использует «кучу фреймов», которые могут сохраняться после возврата вызова функции, который их сделал.

Внутренние функции, такие как addand multiply, которые обращаются к переменным, объявленным во внешней функции ** , называются замыканиями .

Это почти все, что нужно для замыканий.



* Например, он охватывает все пункты в статье «Замыкания для чайников», приведенной в другом ответе , кроме примера 6, в котором просто показано, что переменные можно использовать до того, как они объявлены, - хороший факт, который нужно знать, но совершенно не связанный с замыканиями. Он также охватывает все точки в принятом ответе , за исключением точек (1), в которых функции копируют свои аргументы в локальные переменные (аргументы именованной функции), и (2) что при копировании чисел создается новое число, но копируется ссылка на объект дает вам еще одну ссылку на тот же объект. Это также полезно знать, но опять же совершенно не связано с замыканиями. Это также очень похоже на пример в этом ответе, но немного короче и менее абстрактно. Это не распространяется на точкуэтот ответ или текущего этот комментарийЭто то, что JavaScript затрудняет подключениеЗначение переменной цикла в вашей внутренней функции: шаг «подключения» может быть выполнен только с помощью вспомогательной функции, которая включает вашу внутреннюю функцию и вызывается на каждой итерации цикла. (Строго говоря, внутренняя функция обращается к копии переменной вспомогательной функции, а не подключает что-либо к ней.) Опять же, очень полезно при создании замыканий, но не является частью того, что такое замыкание или как оно работает. Существует дополнительная путаница из-за того, что замыкания работают по-разному в функциональных языках, таких как ML, где переменные связаны со значениями, а не с пространством хранения, предоставляя постоянный поток людей, которые понимают замыкания способом (а именно способом «подключения»), который является просто неверно для JavaScript, где переменные всегда связаны с пространством хранения, а не со значениями.

** Любая внешняя функция, если несколько вложена, или даже в глобальном контексте, как ясно указывает этот ответ .

Мэтт
источник
Что бы произошло, если бы вы позвонили: second_calculator = first_calculator (); вместо second_calculator = make_calculator (); ? Должно быть так же, верно?
Ронен Фестингер
4
@Ronen: Поскольку first_calculatorэто объект (а не функция), вы не должны использовать круглые скобки second_calculator = first_calculator;, поскольку это присваивание, а не вызов функции. Чтобы ответить на ваш вопрос, тогда будет только один вызов make_calculator, поэтому будет выполнен только один калькулятор, а переменные first_calculator и second_calculator будут ссылаться на один и тот же калькулятор, поэтому ответы будут 3, 403, 4433, 44330.
Мэтт
204

Как бы я объяснил это шестилетнему ребенку:

Вы знаете, как взрослые могут владеть домом, и они называют его домом? Когда у мамы есть ребенок, ребенок на самом деле ничего не имеет, верно? Но его родители владеют домом, поэтому, когда кто-то спрашивает ребенка «Где твой дом?», Он / она может ответить «этот дом!» И указать на дом его родителей. «Закрытие» - это способность ребенка всегда (даже если он находится за границей) быть в состоянии сказать, что у него есть дом, даже если дом действительно принадлежит родителю.

Magne
источник
200

Можете ли вы объяснить закрытие 5-летнего ребенка? *

Я все еще думаю, что объяснение Google работает очень хорошо и сжато:

/*
*    When a function is defined in another function and it
*    has access to the outer function's context even after
*    the outer function returns.
*
* An important concept to learn in JavaScript.
*/

function outerFunction(someNum) {
    var someString = 'Hey!';
    var content = document.getElementById('content');
    function innerFunction() {
        content.innerHTML = someNum + ': ' + someString;
        content = null; // Internet Explorer memory leak for DOM reference
    }
    innerFunction();
}

outerFunction(1);​

Доказательство того, что этот пример создает замыкание, даже если внутренняя функция не возвращает

* AC # вопрос

Chris S
источник
11
Код является «правильным», как пример замыкания, даже если он не затрагивает часть комментария об использовании замыкания после возврата externalFunction. Так что это не отличный пример. Есть много других способов использования замыкания, которые не требуют возврата внутренней функции. например, innerFunction может быть передана другой функции, где она вызывается немедленно или сохраняется и вызывается через некоторое время, и во всех случаях она имеет доступ к контексту externalFunction, который был создан при ее вызове.
dlaliberte
6
@syockit Нет, Мосс не прав. Замыкание создается независимо от того, экранируется ли когда-либо функция от области, в которой она определена, и безоговорочно созданная ссылка на лексическую среду родителя делает все переменные в родительской области доступными для всех функций, независимо от того, вызываются они снаружи или внутри область, в которой они были созданы.
Асад Саидуддин
176

Я склонен учиться лучше, сравнивая ХОРОШЕЕ / ПЛОХОЕ. Мне нравится видеть рабочий код, сопровождаемый нерабочим кодом, с которым кто-то может столкнуться. Я собрал jsFiddle, который делает сравнение и пытается свести различия к простейшим объяснениям, которые я смог придумать.

Затворы сделаны правильно:

console.log('CLOSURES DONE RIGHT');

var arr = [];

function createClosure(n) {
    return function () {
        return 'n = ' + n;
    }
}

for (var index = 0; index < 10; index++) {
    arr[index] = createClosure(index);
}

for (var index in arr) {
    console.log(arr[index]());
}
  • В приведенном выше коде createClosure(n)вызывается в каждой итерации цикла. Обратите внимание, что я назвал переменную, nчтобы подчеркнуть, что это новая переменная, созданная в новой области действия функции и не та переменная, indexкоторая связана с внешней областью действия.

  • Это создает новую область и nпривязывается к ней; это означает, что у нас есть 10 отдельных областей, по одной на каждую итерацию.

  • createClosure(n) возвращает функцию, которая возвращает n в этой области.

  • Внутри каждой области видимости nпривязано к любому значению, которое было у нее при createClosure(n)вызове, поэтому возвращаемая вложенная функция всегда будет возвращать значение, nкоторое она имела при createClosure(n)вызове.

Замыкания сделаны неправильно:

console.log('CLOSURES DONE WRONG');

function createClosureArray() {
    var badArr = [];

    for (var index = 0; index < 10; index++) {
        badArr[index] = function () {
            return 'n = ' + index;
        };
    }
    return badArr;
}

var badArr = createClosureArray();

for (var index in badArr) {
    console.log(badArr[index]());
}
  • В приведенном выше коде цикл был перемещен внутри createClosureArray()функции, и теперь функция просто возвращает законченный массив, который на первый взгляд кажется более интуитивным.

  • Что может быть неочевидным, так это то, что поскольку createClosureArray()он вызывается только один раз, для этой функции создается только одна область видимости, а не одна для каждой итерации цикла.

  • Внутри этой функции определена переменная с именем index. Цикл запускается и добавляет функции в массив, которые возвращаются index. Обратите внимание, что indexэто определено внутри createClosureArrayфункции, которая вызывается только один раз.

  • Поскольку в функции была только одна область действия createClosureArray(), indexона связана только со значением в этой области. Другими словами, каждый раз, когда цикл изменяет значение index, он меняет его для всего, что ссылается на него в этой области.

  • Все функции, добавленные в массив, возвращают ту же самую indexпеременную из родительской области, где она была определена, вместо 10 разных из 10 разных областей, как в первом примере. Конечным результатом является то, что все 10 функций возвращают одну и ту же переменную из одной и той же области видимости.

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

Результат

ЗАКРЫТИЯ СДЕЛАНЫ ПРАВО
n = 0
n = 1
n = 2
n = 3
n = 4
n = 5
n = 6
n = 7
n = 8
n = 9

ЗАКРЫТИЯ СДЕЛАНЫ НЕПРАВИЛЬНО
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10
n = 10

Chev.
источник
1
Хорошее дополнение, спасибо. Просто для ясности можно представить, как «плохой» массив создается в «плохом» цикле с каждой итерацией: 1-я итерация: [function () {return 'n =' + 0;}] 2-я итерация: [( function () {return 'n =' + 1;}), (function () {return 'n =' + 1;})] 3-я итерация: [(function () {return 'n =' + 2;}) , (function () {return 'n =' + 2;}), (function () {return 'n =' + 2;})] и т. д. Таким образом, каждый раз, когда значение индекса изменяется, оно отражается во всех функциях уже добавлен в массив.
Алексей Алексеев
3
Использование letдля varисправления разницы.
Рупам Датта
Разве здесь «Закрытие сделано правильно» является примером «Закрытие внутри закрытия»?
TechnicalSmile
Я имею в виду, что каждая функция технически является замыканием, но важной частью является то, что функция определяет новую переменную внутри. Функция, которая получает, возвращает только ссылки, nсозданные в новом замыкании. Мы просто возвращаем функцию, чтобы сохранить ее в массиве и вызвать позже.
Chev
Если вы просто хотите сохранить результат в массиве в первой итерации , то вы можете встроить это следующим образом : arr[index] = (function (n) { return 'n = ' + n; })(index);. Но тогда вы сохраняете результирующую строку в массиве, а не функцию для вызова, которая побеждает точку моего примера.
Chev
164

Википедия о замыканиях :

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

Технически, в JavaScript , каждая функция является замыканием . Он всегда имеет доступ к переменным, определенным в окружающей области видимости.

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

Замыкания часто используются для создания функций с некоторыми скрытыми частными данными (но это не всегда так).

var db = (function() {
    // Create a hidden object, which will hold the data
    // it's inaccessible from the outside.
    var data = {};

    // Make a function, which will provide some access to the data.
    return function(key, val) {
        if (val === undefined) { return data[key] } // Get
        else { return data[key] = val } // Set
    }
    // We are calling the anonymous surrounding function,
    // returning the above inner function, which is a closure.
})();

db('x')    // -> undefined
db('x', 1) // Set x to 1
db('x')    // -> 1
// It's impossible to access the data object itself.
// We are able to get or set individual it.

Эмс

В приведенном выше примере используется анонимная функция, которая была выполнена один раз. Но это не должно быть. Это может быть названо (например mkdb) и выполнено позже, генерируя функцию базы данных каждый раз, когда это вызывается. Каждая сгенерированная функция будет иметь свой собственный скрытый объект базы данных. Другой пример использования замыканий - это когда мы не возвращаем функцию, а объект, содержащий несколько функций для разных целей, каждая из которых имеет доступ к одним и тем же данным.

михал
источник
2
Это лучшее объяснение закрытия JavaScript. Должен быть выбранный ответ. Остальные достаточно интересны, но этот на самом деле полезен практическим способом для реальных JavaScript-кодеров.
геоидезическая
136

Я собрал интерактивное руководство по JavaScript, чтобы объяснить, как работают замыкания. Что такое закрытие?

Вот один из примеров:

var create = function (x) {
    var f = function () {
        return x; // We can refer to x here!
    };
    return f;
};
// 'create' takes one argument, creates a function

var g = create(42);
// g is a function that takes no arguments now

var y = g();
// y is 42 here
Натан Уайтхед
источник
128

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

Секреты функций JavaScript - это частные переменные

var parent = function() {
 var name = "Mary"; // secret
}

Каждый раз, когда вы вызываете его, создается локальная переменная «имя» с именем «Мэри». И каждый раз при выходе из функции переменная теряется, а имя забывается.

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

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

var parent = function() {
  var name = "Mary";
  var child = function(childName) {
    // I can also see that "name" is "Mary"
  }
}

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

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

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

var parent = function() {
  var name = "Mary";
  var child = function(childName) {
    return "My name is " + childName  +", child of " + name; 
  }
  return child; // child leaves the parent ->
}
var child = parent(); // < - and here it is outside 

И теперь, несмотря на то, что Мэри «больше не бежит», память о ней не потеряна, и ее ребенок всегда будет помнить ее имя и другие секреты, которыми они поделились во время совместной жизни.

Итак, если вы назовете ребенка "Алиса", она ответит

child("Alice") => "My name is Alice, child of Mary"

Это все, что можно сказать.

Теро Толонен
источник
15
Это объяснение, которое имело для меня наибольшее значение, потому что оно не предполагает значительных предварительных знаний технических терминов. Объяснение, получившее наибольшее количество голосов, предполагает, что человек, который не понимает замыкания, имеет полное и полное понимание таких терминов, как «лексическая область» и «контекст исполнения» - хотя я могу понять их концептуально, я не думаю, что меня устраивают детали их, как мне следовало бы, и объяснение без жаргона - это то, что сделало закрытие, наконец, щелчком для меня, спасибо. В качестве бонуса, я думаю, это также объясняет, что сфера очень кратко.
Эмма W
103

Я не понимаю, почему ответы здесь такие сложные.

Вот закрытие:

var a = 42;

function b() { return a; }

Да. Вы, вероятно, используете это много раз в день.


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

Теперь то, что он позволяет вам делать, может быть более впечатляющим, смотрите другие ответы.

флорибона
источник
5
Этот ответ, похоже, не поможет сбить с толку людей. Грубым эквивалентом в традиционном языке программирования может быть создание b () как метода для объекта, который также имеет частную константу или свойство a. На мой взгляд, удивительным является то, что объект области действия JS эффективно предоставляет aкак свойство, а не константа. И вы заметите это важное поведение, только если измените его, как вreturn a++;
Джон Кумбс
1
Именно то, что сказал Джон. Прежде чем я наконец-то ухватился за замыкания, мне было трудно найти практические примеры. Да, floribon создал закрытие, но для необразованного меня это ничему не научило бы.
Chev
3
Это не определяет, что такое замыкание - это просто пример, в котором оно используется. И это не учитывает нюанс того, что происходит, когда заканчивается область; Я не думаю, что у кого-то возникал вопрос о лексической области видимости, когда все области все еще существуют, особенно в случае глобальной переменной.
Джерард ONeill
92

Пример для первого пункта по dlaliberte:

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

var i;
function foo(x) {
    var tmp = 3;
    i = function (y) {
        console.log(x + y + (++tmp));
    }
}
foo(2);
i(3);
someisaac
источник
Небольшое уточнение о возможной двусмысленности. Когда я сказал: «На самом деле функция включения вообще не должна возвращаться». Я имел в виду не «не возвращать значение», а «все еще активен» Таким образом, пример не показывает этот аспект, хотя он показывает другой способ передачи внутренней функции во внешнюю область. Основной момент, который я пытался сделать, касается времени создания замыкания (для функции включения), поскольку некоторые люди, кажется, думают, что это происходит, когда функция включения возвращается. Другой пример необходим, чтобы показать, что замыкание создается при вызове функции .
dlaliberte
89

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

Ракеш Пай
источник
35
Это только половина объяснения. В отношении замыканий важно отметить, что если на внутреннюю функцию все еще ссылаются после выхода из внешней функции, старые значения внешней функции все еще доступны для внутренней.
pcorcoran
22
На самом деле, это не старые значения внешней функции, которые доступны для внутренней функции, а старые переменные , которые могут иметь новые значения, если какая-то функция смогла их изменить.
dlaliberte
86

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

// makeSequencer will return a "sequencer" function
var makeSequencer = function() {
    var _count = 0; // not accessible outside this function
    var sequencer = function () {
        return _count++;
    }
    return sequencer;
}

var fnext = makeSequencer();
var v0 = fnext();     // v0 = 0;
var v1 = fnext();     // v1 = 1;
var vz = fnext._count // vz = undefined
Gerardo Lima
источник
82

Ты переспал и пригласил Дэна. Вы говорите Дэну принести один контроллер XBox.

Дэн приглашает Пола. Дэн просит Пола принести одного контролера. Сколько контролеров было привезено на вечеринку?

function sleepOver(howManyControllersToBring) {

    var numberOfDansControllers = howManyControllersToBring;

    return function danInvitedPaul(numberOfPaulsControllers) {
        var totalControllers = numberOfDansControllers + numberOfPaulsControllers;
        return totalControllers;
    }
}

var howManyControllersToBring = 1;

var inviteDan = sleepOver(howManyControllersToBring);

// The only reason Paul was invited is because Dan was invited. 
// So we set Paul's invitation = Dan's invitation.

var danInvitedPaul = inviteDan(howManyControllersToBring);

alert("There were " + danInvitedPaul + " controllers brought to the party.");
StewShack
источник
80

Автор Closures довольно хорошо объяснил замыкания, объясняя причину, почему они нам нужны, а также объясняя LexicalEnvironment, что необходимо для понимания замыканий.
Вот резюме:

Что если переменная доступна, но она не локальная? Как здесь:

Введите описание изображения здесь

В этом случае интерпретатор находит переменную во внешнем LexicalEnvironment объекте.

Процесс состоит из двух этапов:

  1. Во-первых, когда функция f создается, она не создается в пустом пространстве. Существует текущий объект LexicalEnvironment. В приведенном выше случае это окно (a не определено во время создания функции).

Введите описание изображения здесь

Когда функция создается, она получает скрытое свойство с именем [[Scope]], которое ссылается на текущую LexicalEnvironment.

Введите описание изображения здесь

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

Вложенные функции

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

Введите описание изображения здесь

Итак, функция g имеет доступ к g, a и f.

Затворы

Вложенная функция может продолжать жить после завершения внешней функции:

Введите описание изображения здесь

Разметка LexicalEnvironments:

Введите описание изображения здесь

Как мы видим, this.say это свойство объекта user, поэтому оно продолжает жить после завершения User.

И если вы помните, когда this.sayон создан, он (как и каждая функция) получает внутреннюю ссылку this.say.[[Scope]]на текущую LexicalEnvironment. Таким образом, LexicalEnvironment текущего выполнения пользователя остается в памяти. Все переменные User также являются его свойствами, поэтому они также тщательно сохраняются, а не отбрасываются, как обычно.

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

Обобщить:

  1. Внутренняя функция сохраняет ссылку на внешнюю LexicalEnvironment.
  2. Внутренняя функция может обращаться к переменным из нее в любое время, даже если внешняя функция завершена.
  3. Браузер сохраняет LexicalEnvironment и все его свойства (переменные) в памяти, пока не найдется внутренняя функция, которая ссылается на него.

Это называется закрытием.

Arvand
источник
78

Функции JavaScript могут получить доступ к своим:

  1. аргументы
  2. Локальные (то есть их локальные переменные и локальные функции)
  3. Среда, которая включает в себя:
    • глобалы, включая DOM
    • что-нибудь во внешних функциях

Если функция обращается к своей среде, то функция является замыканием.

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

Пример замыкания, использующего глобальную среду:

Представьте себе, что события кнопок «Переполнение стека» «Голосование вверх» и «Голосование вниз» реализованы в виде замыканий, voiceUp_click и voiceDown_click, которые имеют доступ к внешним переменным isVotedUp и isVotedDown, которые определены глобально. (Для простоты я имею в виду кнопки «Голосовать» в StackOverflow, а не массив кнопок «Голосовать».)

Когда пользователь нажимает кнопку VoteUp, функция voiceUp_click проверяет, является ли isVotedDown == true, чтобы определить, следует ли голосовать за или просто отменить голосование с понижением. Функция VoteUp_click является закрытием, потому что она обращается к своей среде.

var isVotedUp = false;
var isVotedDown = false;

function voteUp_click() {
  if (isVotedUp)
    return;
  else if (isVotedDown)
    SetDownVote(false);
  else
    SetUpVote(true);
}

function voteDown_click() {
  if (isVotedDown)
    return;
  else if (isVotedUp)
    SetUpVote(false);
  else
    SetDownVote(true);
}

function SetUpVote(status) {
  isVotedUp = status;
  // Do some CSS stuff to Vote-Up button
}

function SetDownVote(status) {
  isVotedDown = status;
  // Do some CSS stuff to Vote-Down button
}

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

Джон Пик
источник
59

Как отец 6-летнего ребенка, который в настоящее время обучает детей младшего возраста (и относительного новичка в области кодирования без формального образования, поэтому требуются исправления), я думаю, что этот урок лучше всего использовать в практической игре. Если шестилетний ребенок готов понять, что такое закрытие, тогда он достаточно взрослый, чтобы пойти самому. Я бы предложил вставить код в jsfiddle.net, немного пояснить и оставить их одних, чтобы придумать уникальную песню. Пояснительный текст ниже, вероятно, больше подходит для 10-летнего.

function sing(person) {

    var firstPart = "There was " + person + " who swallowed ";

    var fly = function() {
        var creature = "a fly";
        var result = "Perhaps she'll die";
        alert(firstPart + creature + "\n" + result);
    };

    var spider = function() {
        var creature = "a spider";
        var result = "that wiggled and jiggled and tickled inside her";
        alert(firstPart + creature + "\n" + result);
    };

    var bird = function() {
        var creature = "a bird";
        var result = "How absurd!";
        alert(firstPart + creature + "\n" + result);
    };

    var cat = function() {
        var creature = "a cat";
        var result = "Imagine That!";
        alert(firstPart + creature + "\n" + result);
    };

    fly();
    spider();
    bird();
    cat();
}

var person="an old lady";

sing(person);

ИНСТРУКЦИИ

ДАННЫЕ: Данные представляют собой набор фактов. Это могут быть цифры, слова, измерения, наблюдения или даже просто описания вещей. Вы не можете прикоснуться к этому, почувствовать запах или попробовать это. Вы можете записать это, сказать это и услышать это. Вы можете использовать его для создания сенсорный запах и вкус, используя компьютер. Это может быть полезно на компьютере с помощью кода.

КОД: Все написанное выше называется кодом . Это написано в JavaScript.

JAVASCRIPT: JavaScript это язык. Как английский или французский или китайский языки. Есть много языков, которые понимают компьютеры и другие электронные процессоры. Для того чтобы JavaScript был понятен компьютеру, ему нужен переводчик. Представьте, что учитель, который говорит только по-русски, приходит преподавать ваш класс в школе. Когда учитель говорит «все садятся», класс не понимает. Но, к счастью, у вас в классе есть русский ученик, который говорит всем, что это означает, что «все садятся» - так вы все делаете. Класс подобен компьютеру, а русский ученик - переводчик. Для JavaScript самый распространенный интерпретатор называется браузером.

БРАУЗЕР: при подключении к Интернету на компьютере, планшете или телефоне для посещения веб-сайта вы используете браузер. Примеры, которые вы можете знать, это Internet Explorer, Chrome, Firefox и Safari. Браузер может понимать JavaScript и сообщать компьютеру, что ему нужно делать. Инструкции JavaScript называются функциями.

ФУНКЦИЯ: функция в JavaScript похожа на фабрику. Это может быть небольшая фабрика с одной машиной внутри. Или это может быть много других маленьких фабрик, на каждом из которых много машин делают разные работы. На реальной фабрике одежды у вас могут быть пачки тканей и бобин ниток, а также футболки и джинсы. Наша фабрика JavaScript обрабатывает только данные, она не может шить, сверлить или расплавлять металл. В нашей фабрике JavaScript данные поступают, а данные выходят.

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

У меня нет времени ходить по магазинам, поэтому функция должна знать, что у нас в холодильнике, чтобы принимать решения. Каждый ингредиент имеет разное время приготовления, и мы хотим, чтобы робот одновременно подавал все в горячем виде. Нам нужно предоставить функции данные о том, что нам нравится, функция может «разговаривать» с холодильником, а функция может управлять роботом.

Функция обычно имеет имя, скобки и фигурные скобки. Нравится:

function cookMeal() {  /*  STUFF INSIDE THE FUNCTION  */  }

Обратите внимание, что /*...*/и //остановите код, читаемый браузером.

ИМЯ: Вы можете вызывать функцию практически из любого нужного вам слова. Пример «cookMeal» типичен для объединения двух слов и введения второго заглавной буквы в начале, но это не обязательно. В нем не может быть пробела, и это не может быть число само по себе.

РОДИТЕЛИ: «круглые скобки» или ()почтовый ящик на двери фабрики функций JavaScript или почтовый ящик на улице для отправки пакетов информации на фабрику. Иногда почтовый ящик может быть отмечен, например cookMeal(you, me, yourFriend, myFriend, fridge, dinnerTime) , в этом случае вы знаете, какие данные вы должны предоставить.

BRACES: «Подтяжки», которые выглядят так, {}- это тонированные стекла нашей фабрики. Внутри фабрики вы можете видеть снаружи, но снаружи вы не можете видеть внутри.

Пример длинного кода выше

Наш код начинается со слова function , поэтому мы знаем, что он один! Затем название функции sing - это мое собственное описание того, о чем эта функция. Тогда скобки () . Скобки всегда есть для функции. Иногда они пусты, и иногда у них есть что - то в этом один имеет слово.: (person). После этого есть такая скобка {. Это отмечает начало функции sing () . У него есть партнер, который отмечает конец Sing (), как это}

function sing(person) {  /* STUFF INSIDE THE FUNCTION */  }

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

Теперь, после функции sing () , ближе к концу кода находится строка

var person="an old lady";

VARIABLE: буквы var означают «переменная». Переменная похожа на конверт. Снаружи этот конверт помечен как «человек». Внутри он содержит листок бумаги с информацией, которая нужна нашей функции, некоторые буквы и пробелы, соединенные вместе, как кусок строки (это называется строкой), что делает фразу «старая женщина». Наш конверт может содержать другие виды вещей, такие как числа (называемые целыми числами), инструкции (называемые функциями), списки (называемые массивами ). Поскольку эта переменная записана вне всех фигурных скобок {}, и поскольку вы можете видеть сквозь тонированные окна, когда находитесь внутри фигурных скобок, эту переменную можно увидеть из любого места в коде. Мы называем это «глобальной переменной».

ГЛОБАЛЬНАЯ ПЕРЕМЕННАЯ: человек - это глобальная переменная, означающая, что если вы измените ее значение со «пожилой женщины» на «молодой человек», человек будет оставаться молодым человеком, пока вы не решите изменить его снова и что любая другая функция в Код может видеть, что это молодой человек. Нажмите F12кнопку или посмотрите настройки параметров, чтобы открыть консоль разработчика в браузере, и введите «person», чтобы увидеть, что это за значение. Напечатайте, person="a young man"чтобы изменить это, и затем напечатайте "человека" снова, чтобы видеть, что это изменилось.

После этого у нас есть линия

sing(person);

Эта строка вызывает функцию, как если бы она вызывала собаку

"Давай петь , иди и получить человека !"

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

Функции определяют действия - основная функция - пение. Он содержит переменную под названием firstPart, которая применяется к пению о человеке, которое относится к каждому из стихов песни: «Был« + человек + », который проглотил». Если вы введете firstPart в консоли, вы не получите ответ, потому что переменная заблокирована в функции - браузер не может видеть внутри окрашенных окон фигурных скобок.

ЗАКРЫТИЯ: замыкания - это меньшие функции, которые находятся внутри большой функции sing () . Маленькие фабрики внутри большой фабрики. Каждый из них имеет свои собственные скобки, которые означают, что переменные внутри них не видны снаружи. Вот почему имена переменных ( существо и результат ) могут повторяться в замыканиях, но с разными значениями. Если вы введете эти имена переменных в окне консоли, вы не получите их значение, потому что оно скрыто двумя слоями тонированных окон.

Все замыкания знают, что такое переменная функции sing () под названием firstPart , потому что они могут видеть из своих тонированных окон.

После закрытия идут линии

fly();
spider();
bird();
cat();

Функция sing () будет вызывать каждую из этих функций в том порядке, в котором они заданы. Тогда работа функции sing () будет выполнена.

благодарны
источник
56

Ладно, разговаривая с 6-летним ребенком, я бы использовал следующие ассоциации.

Представьте себе - вы играете со своими маленькими братьями и сестрами по всему дому, вы ходите со своими игрушками и приносите некоторые из них в комнату вашего старшего брата. Через некоторое время ваш брат вернулся из школы и пошел в свою комнату, и он запер ее внутри, так что теперь вы больше не могли получить прямой доступ к оставленным там игрушкам. Но вы можете постучать в дверь и попросить вашего брата за эти игрушки. Это называется закрытием игрушки ; твой брат сделал это для тебя, и теперь он во внешнем масштабе .

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

Для продвинутого ребенка я бы поставил что-то вроде следующего. Это не идеально, но это заставляет вас чувствовать, что это такое:

function playingInBrothersRoom (withToys) {
  // We closure toys which we played in the brother's room. When he come back and lock the door
  // your brother is supposed to be into the outer [[scope]] object now. Thanks god you could communicate with him.
  var closureToys = withToys || [],
      returnToy, countIt, toy; // Just another closure helpers, for brother's inner use.

  var brotherGivesToyBack = function (toy) {
    // New request. There is not yet closureToys on brother's hand yet. Give him a time.
    returnToy = null;
    if (toy && closureToys.length > 0) { // If we ask for a specific toy, the brother is going to search for it.

      for ( countIt = closureToys.length; countIt; countIt--) {
        if (closureToys[countIt - 1] == toy) {
          returnToy = 'Take your ' + closureToys.splice(countIt - 1, 1) + ', little boy!';
          break;
        }
      }
      returnToy = returnToy || 'Hey, I could not find any ' + toy + ' here. Look for it in another room.';
    }
    else if (closureToys.length > 0) { // Otherwise, just give back everything he has in the room.
      returnToy = 'Behold! ' + closureToys.join(', ') + '.';
      closureToys = [];
    }
    else {
      returnToy = 'Hey, lil shrimp, I gave you everything!';
    }
    console.log(returnToy);
  }
  return brotherGivesToyBack;
}
// You are playing in the house, including the brother's room.
var toys = ['teddybear', 'car', 'jumpingrope'],
    askBrotherForClosuredToy = playingInBrothersRoom(toys);

// The door is locked, and the brother came from the school. You could not cheat and take it out directly.
console.log(askBrotherForClosuredToy.closureToys); // Undefined

// But you could ask your brother politely, to give it back.
askBrotherForClosuredToy('teddybear'); // Hooray, here it is, teddybear
askBrotherForClosuredToy('ball'); // The brother would not be able to find it.
askBrotherForClosuredToy(); // The brother gives you all the rest
askBrotherForClosuredToy(); // Nothing left in there

Как видите, игрушки, оставленные в комнате, по-прежнему доступны через брата, и не важно, заперта ли комната. Вот jsbin, чтобы поиграть с ним.

dmi3y
источник
49

Ответ для шестилетнего ребенка (при условии, что он знает, что такое функция, что такое переменная и какие данные):

Функции могут возвращать данные. Один вид данных, который вы можете вернуть из функции - это другая функция. Когда эта новая функция возвращается, все переменные и аргументы, используемые в функции, которая ее создала, не исчезают. Вместо этого родительская функция «закрывается». Другими словами, ничто не может заглянуть внутрь него и увидеть переменные, которые оно использовало, кроме функции, которую оно возвратило. Эта новая функция имеет особую возможность заглянуть внутрь функции, которая ее создала, и просмотреть данные внутри нее.

function the_closure() {
  var x = 4;
  return function () {
    return x; // Here, we look back inside the_closure for the value of x
  }
}

var myFn = the_closure();
myFn(); //=> 4

Другой действительно простой способ объяснить это с точки зрения объема:

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

Глупо Глупо
источник
49

Функция в JavaScript - это не просто ссылка на набор инструкций (как в языке C), но она также включает в себя скрытую структуру данных, которая состоит из ссылок на все нелокальные переменные, которые она использует (захваченные переменные). Такие двухсекционные функции называются замыканиями. Каждая функция в JavaScript может считаться закрытием.

Замыкания являются функциями с состоянием. Это несколько похоже на «это» в том смысле, что «это» также предоставляет состояние для функции, но функция и «это» являются отдельными объектами («это» - просто причудливый параметр, и единственный способ навсегда связать его с функция заключается в создании замыкания). Хотя «this» и функция всегда живут отдельно, функцию нельзя отделить от ее закрытия, и язык не предоставляет средств для доступа к захваченным переменным.

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

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

Пример:

function foo (initValue) {
   //This variable is not destroyed when the foo function exits.
   //It is 'captured' by the two nested functions returned below.
   var value = initValue;

   //Note that the two returned functions are created right now.
   //If the foo function is called again, it will return
   //new functions referencing a different 'value' variable.
   return {
       getValue: function () { return value; },
       setValue: function (newValue) { value = newValue; }
   }
}

function bar () {
    //foo sets its local variable 'value' to 5 and returns an object with
    //two functions still referencing that local variable
    var obj = foo(5);

    //Extracting functions just to show that no 'this' is involved here
    var getValue = obj.getValue;
    var setValue = obj.setValue;

    alert(getValue()); //Displays 5
    setValue(10);
    alert(getValue()); //Displays 10

    //At this point getValue and setValue functions are destroyed
    //(in reality they are destroyed at the next iteration of the garbage collector).
    //The local variable 'value' in the foo is no longer referenced by
    //anything and is destroyed too.
}

bar();
srgstm
источник
47

Возможно, немного за пределами всего, кроме самого раннего из шестилетних, но несколько примеров, которые помогли сделать концепцию замыкания в JavaScript, меня зацепили.

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

function outerFunction() {
    var outerVar = "monkey";
    
    function innerFunction() {
        alert(outerVar);
    }
    
    innerFunction();
}

outerFunction();

ALERT: обезьяна

В приведенном выше примере вызывается externalFunction, которая, в свою очередь, вызывает innerFunction. Обратите внимание, что externalVar доступен для innerFunction, о чем свидетельствует его правильное оповещение о значении externalVar.

Теперь рассмотрим следующее:

function outerFunction() {
    var outerVar = "monkey";
    
    function innerFunction() {
        return outerVar;
    }
    
    return innerFunction;
}

var referenceToInnerFunction = outerFunction();
alert(referenceToInnerFunction());

ALERT: обезьяна

referenceToInnerFunction установлен в externalFunction (), которая просто возвращает ссылку на innerFunction. Когда вызывается referenceToInnerFunction, она возвращает externalVar. Опять же, как и выше, это демонстрирует, что innerFunction имеет доступ к outerVar, переменной externalFunction. Кроме того, интересно отметить, что он сохраняет этот доступ даже после того, как externalFunction завершит выполнение.

И вот тут все становится действительно интересно. Если бы мы избавились от externalFunction, скажем, установили его в null, вы могли бы подумать, что referenceToInnerFunction потеряет свой доступ к значению externalVar. Но это не так.

function outerFunction() {
    var outerVar = "monkey";
    
    function innerFunction() {
        return outerVar;
    }
    
    return innerFunction;
}

var referenceToInnerFunction = outerFunction();
alert(referenceToInnerFunction());

outerFunction = null;
alert(referenceToInnerFunction());

ALERT: обезьяна ALERT: обезьяна

Но как это так? Как referenceToInnerFunction может все еще знать значение externalVar теперь, когда externalFunction был установлен в нуль?

Причина, по которой referenceToInnerFunction по-прежнему может обращаться к значению externalVar, заключается в том, что, когда замыкание было впервые создано путем размещения innerFunction внутри externalFunction, innerFunction добавила ссылку на область действия externalFunction (ее переменные и функции) в свою цепочку областей действия. Это означает, что innerFunction имеет указатель или ссылку на все переменные externalFunction, включая externalVar. Таким образом, даже когда externalFunction закончила выполнение или даже если она удалена или установлена ​​в значение null, переменные в ее области видимости, такие как outerVar, остаются в памяти из-за выдающейся ссылки на них со стороны внутренней функции, которая была возвращена referenceToInnerFunction. Чтобы по-настоящему освободить externalVar и остальные переменные externalFunction из памяти, вам нужно избавиться от этой выдающейся ссылки на них,

//////////

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

function outerFunction() {
    var outerVar = "monkey";
    
    function innerFunction() {
        alert(outerVar);
    }
    
    outerVar = "gorilla";

    innerFunction();
}

outerFunction();

ALERT: горилла

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

Michael Dziedzic
источник
45

Я бы просто указал им на страницу "Мозилла" . Это лучшее, самое краткое и простое объяснение основ закрытия и практического использования, которое я нашел. Настоятельно рекомендуется всем, кто изучает JavaScript.

И да, я бы даже порекомендовал это 6-летнему ребенку - если 6-летний учится о замыканиях, то логично, что они готовы понять краткое и простое объяснение, приведенное в статье.

mjmoody383
источник
Я согласен: упомянутая страница Mozilla особенно проста и лаконична. Удивительно, но ваш пост не был оценен так широко, как другие.
Брайс Кустильяс