Питання Як працюють закриття JavaScript?


Як би ви пояснили закриття JavaScript для того, хто знає поняття, яке вони складаються (наприклад, функції, змінні тощо), але не розуміють самі закриття?

я бачив приклад схеми дані на Вікіпедії, але, на жаль, це не допомогло.


7654


походження


Моя проблема з цими та багатьма відповідями полягає в тому, що вони підходять до нього з абстрактної, теоретичної точки зору, а не починають з пояснення просто, чому у Javascript необхідні замикання та практичні ситуації, в яких ви їх використовуєте. Ви в кінцевому підсумку стверджує, що ви повинні спішно, завжди думати, "але чому?". Я б просто почав: закриття - це акуратний спосіб вирішення наступних двох реалій JavaScript: a. область дії на рівні функції, а не рівень блоків і, б. значна частина того, що ви робите на практиці в JavaScript, є асинхронною / керованою подією. - Jeremy Burton
@Redsandro Для одного, це робить подій керований код набагато простіше писати. Я можу запустити функцію під час завантаження сторінки, щоб визначити специфіку щодо HTML або доступних функцій. Я можу визначити і встановити обробник у цій функції, і мати доступ до цієї інформації контексту кожного разу, коли викликається обробник, не маючи повторного запиту. Вирішити проблему один раз, повторно використати на кожній сторінці, де цей обробник потрібен, з меншим накладними витратами на повторному виклику обробника. Ви коли-небудь бачите ті ж самі дані, повторно зіставлені два рази на мові, яка їх не має? Закриття робить його набагато легше уникнути такого роду речі. - Erik Reppen
Для програмістів Java коротка відповідь полягає в тому, що це еквівалент функції внутрішнього класу. Внутрішній клас також містить неявний покажчик на екземпляр зовнішнього класу і використовується для цілком однакової мети (тобто, створюючи обробники подій). - Boris van Schooten
Це зрозуміло звідси краще: javascriptissexy.com/understand-javascript-closures-with-ease. Ще потрібно а закриття після закриття після прочитання інших відповідей. :) - Akhoy
Я виявив, що цей практичний приклад дуже корисний: youtube.com/watch?v=w1s9PgtEoJs - Abhi


Відповіді:


Закриття JavaScript для початківців

Представлено Моррісом у Вт, 2006-02-21 10:19. Community-edited since.

Замикання не магічні

На цій сторінці пояснюються закриття, щоб програміст їх зрозумів - використовуючи працюючий код JavaScript. Це не для гуру чи функціональних програмістів.

Закриття є не важко щоб зрозуміти, як тільки основна концепція вихоплюється. Проте їх неможливо зрозуміти, прочитавши будь-які академічні документи або академічно орієнтовану інформацію про них!

Ця стаття призначена для програмістів із певним досвідом програмування на основній мові та хто може читати наступну функцію JavaScript:

function sayHello(name) {
  var text = 'Hello ' + name;
  var say = function() { console.log(text); }
  say();
}
sayHello('Joe');

Приклад закриття

Два одного резюме резюме:

  • Закриття є одним із способів підтримки першокласні функції; це вираз, який може посилання змінних у межах його обсягу (коли він був оголошений першим), бути призначений для змінної, передавати як аргумент функції або бути повернутим як результат функції.

  • Або закриття - кадр стека, який виділяється, коли функція починає виконання, і не звільнений після повернення функції (начебто "кадр стека" було виділено на купі, а не на стек!).

Наступний код повертає посилання на функцію:

function sayHello2(name) {
  var text = 'Hello ' + name; // Local variable
  var say = function() { console.log(text); }
  return say;
}
var say2 = sayHello2('Bob');
say2(); // logs "Hello Bob"

Більшість програмістів JavaScript розуміють, як посилання на функцію повертається до змінної (say2) у наведеному вище коді. Якщо ви цього не зробите, тоді вам потрібно подивитися на це, перш ніж ви зможете дізнатися про закриття. Програміст, що використовує C, міг би подумати про функцію, як про повернення покажчика на функцію, а також про змінні say і say2 були кожним вказівником на функцію.

Існує критична різниця між C-покажчиком до функції та посиланням JavaScript на функцію. У JavaScript ви можете подумати про функціональну посиланням змінної як на покажчик на функцію так само як прихований покажчик на закриття.

Вищезгаданий код має закриття через анонімну функцію function() { console.log(text); } оголошується всередині інша функція sayHello2() в цьому прикладі. У JavaScript, якщо ви використовуєте function ключове слово всередині іншої функції, ви створюєте закриття.

У С і більшості інших загальних мов після функція повертає, всі локальні змінні більше не доступні, оскільки кадр стека знищений.

У JavaScript, якщо ви оголошувати функцію в іншій функції, то локальні змінні можуть залишатися доступними після повернення з функції, яку ви зателефонували. Це продемонстровано вище, оскільки ми називаємо функцію say2()після того, як ми повернулися з sayHello2(). Зверніть увагу, що код, який ми називаємо посиланням змінної text, який був а локальна змінна функції sayHello2().

function() { console.log(text); } // Output of say2.toString();

Дивлячись на вихід з say2.toString(), ми можемо бачити, що код відноситься до змінної text. Анонімна функція може посилатися text що має значення 'Hello Bob' тому що локальні змінні sayHello2() зберігаються в закритті.

Магія полягає в тому, що в JavaScript посилання на функцію також має таємне посилання на закриття, яке воно було створено, - подібно до того, як делегати є покажчиком методу плюс таємне посилання на об'єкт.

Більше прикладів

З деяких причин закриття здаються важко зрозуміти, коли ви читаєте про них, але коли ви бачите деякі приклади, стає зрозумілим, як вони працюють (це займає деякий час). Я рекомендую уважно розглянути приклади, поки не зрозумієте, як вони працюють. Якщо ви починаєте використовувати закривальні пристрої без повного розуміння того, як вони працюють, ви скоро створите кілька дуже дивних помилок!

Приклад 3

Цей приклад показує, що локальні змінні не копіюються - вони зберігаються за посиланням. Це нагадує збереження стек-кадру в пам'яті, коли зовнішня функція виходить!

function say667() {
  // Local variable that ends up within closure
  var num = 42;
  var say = function() { console.log(num); }
  num++;
  return say;
}
var sayNumber = say667();
sayNumber(); // logs 43

Приклад 4

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

var gLogNumber, gIncreaseNumber, gSetNumber;
function setupSomeGlobals() {
  // Local variable that ends up within closure
  var num = 42;
  // Store some references to functions as global variables
  gLogNumber = function() { console.log(num); }
  gIncreaseNumber = function() { num++; }
  gSetNumber = function(x) { num = x; }
}

setupSomeGlobals();
gIncreaseNumber();
gLogNumber(); // 43
gSetNumber(5);
gLogNumber(); // 5

var oldLog = gLogNumber;

setupSomeGlobals();
gLogNumber(); // 42

oldLog() // 5

Ці три функції мають загальний доступ до одного і того ж закриття - локальні змінні setupSomeGlobals() коли були визначені три функції.

Зауважте, що в наведеному вище прикладі, якщо ви телефонуєте setupSomeGlobals() знову ж, створюється нове закриття (стек-кадр!). Старий gLogNumber, gIncreaseNumber, gSetNumber змінні перезаписуються новий функції, які мають нове закриття. (У JavaScript, щоразу, коли ви оголошуєте функцію всередині іншої функції, внутрішня функція (и) / знову створюється кожен час виклику зовнішньої функції.)

Приклад 5

Цей приклад показує, що закриття містить будь-які локальні змінні, які були оголошені всередині зовнішньої функції, перш ніж він закінчився. Зауважте, що змінна alice фактично оголошено після анонімної функції. Анонімна функція оголошена першою; і коли ця функція називається вона може отримати доступ до alice змінна через alice знаходиться в тій же обсязі (JavaScript робить змінний підйом) Також sayAlice()() просто безпосередньо викликає посилання функції, що повертається з sayAlice() - це точно так само, як і раніше, але без тимчасової змінної.

function sayAlice() {
    var say = function() { console.log(alice); }
    // Local variable that ends up within closure
    var alice = 'Hello Alice';
    return say;
}
sayAlice()();// logs "Hello Alice"

Хитромудрі: зверніть увагу також на те, що say змінна також знаходиться всередині закриття, і може бути доступ до будь-якої іншої функції, яка може бути оголошена в межах sayAlice(), або він може бути доступний рекурсивно всередині функції.

Приклад 6

Ця людина є справжньою готкою для багатьох людей, тому вам потрібно це зрозуміти. Будьте обережні, якщо ви визначаєте функцію в циклі: локальні змінні з закриття можуть не діяти, як ви можете спочатку думати.

Щоб зрозуміти цей приклад, потрібно зрозуміти функцію "переміщення піднімання" в Javascript.

function buildList(list) {
    var result = [];
    for (var i = 0; i < list.length; i++) {
        var item = 'item' + i;
        result.push( function() {console.log(item + ' ' + list[i])} );
    }
    return result;
}

function testList() {
    var fnlist = buildList([1,2,3]);
    // Using j only to help prevent confusion -- could use i.
    for (var j = 0; j < fnlist.length; j++) {
        fnlist[j]();
    }
}

 testList() //logs "item2 undefined" 3 times

Лінія result.push( function() {console.log(item + ' ' + list[i])} додає масиву результатів тричі до анонімної функції. Якщо ви не дуже знайомі з анонімними функціями, то подумайте про це:

pointer = function() {console.log(item + ' ' + list[i])};
result.push(pointer);

Зауважте, що при запуску прикладу "item2 undefined" зареєстровано три рази! Це тому, що, як і попередні приклади, існує лише одне замикання для локальних змінних для buildList (які є result, i і item) Коли на лінії називаються анонімні функції fnlist[j](); вони всі використовують одне і те ж закривання, і вони використовують поточне значення для i і item в межах цього одного закриття (де i має значення 3 тому що петля була завершена, і item має значення 'item2') Зауважте, що ми індексуємо від 0, отже item має значення item2. І I ++ буде збільшуватися i до вартості 3.

Можливо, буде корисно подивитися, що трапляється, коли на рівні блоку оголошується змінна item використовується (через let ключове слово), а не декларування змінної функції за допомогою функції var ключове слово Якщо ця зміна виконана, то кожна анонімна функція в масиві result має своє закриття; при виконанні прикладу виведення виглядає наступним чином:

item0 undefined
item1 undefined
item2 undefined

Якщо змінна i також визначається використанням let замість var, то вихідний результат:

item0 1
item1 2
item2 3

Приклад 7

У цьому останньому прикладі кожний виклик до основної функції створює окреме закриття.

function newClosure(someNum, someRef) {
    // Local variables that end up within closure
    var num = someNum;
    var anArray = [1,2,3];
    var ref = someRef;
    return function(x) {
        num += x;
        anArray.push(num);
        console.log('num: ' + num +
            '; anArray: ' + anArray.toString() +
            '; ref.someVar: ' + ref.someVar + ';');
      }
}
obj = {someVar: 4};
fn1 = newClosure(4, obj);
fn2 = newClosure(5, obj);
fn1(1); // num: 5; anArray: 1,2,3,5; ref.someVar: 4;
fn2(1); // num: 6; anArray: 1,2,3,6; ref.someVar: 4;
obj.someVar++;
fn1(2); // num: 7; anArray: 1,2,3,5,7; ref.someVar: 5;
fn2(2); // num: 8; anArray: 1,2,3,6,8; ref.someVar: 5;

Резюме

Якщо все здається абсолютно незрозумілим, то найкраще, щоб це зробити, - це грати з прикладами. Читання пояснення набагато важче, ніж розуміння прикладів. Мої пояснення про закриття та стек-кадри тощо не є технічно правильними - вони є великими спрощеннями, які допомагають зрозуміти. Після того, як основну ідею буде заховано, ви зможете отримати деталі пізніше.

Кінцеві моменти:

  • Кожного разу, коли ви використовуєте function всередині іншої функції використовується закриття.
  • Кожного разу, коли ви використовуєте eval() всередині функції, використовується закриття. Текст у вас eval може посилатися на локальні змінні функції та всередині eval ви можете навіть створити нові локальні змінні за допомогою eval('var foo = …')
  • Коли ви використовуєте new Function(…) (the Конструктор функцій) всередині функції, вона не створює закриття. (Нова функція не може посилатися на локальні змінні зовнішньої функції.)
  • Закриття в JavaScript схоже на збереження копії всіх локальних змінних, як і тоді, коли вийшла функція.
  • Напевно, краще думати, що закриття завжди створюється просто входом до функції, а локальні змінні додаються до цього закриття.
  • Новий набір локальних змінних зберігається кожного разу, коли викликається функція з замиканням (з урахуванням того, що функція містить декларацію функції всередині неї, і посилання на цю внутрішню функцію або повертається, або зовнішня посилання зберігається для нього певним чином )
  • Дві функції можуть виглядати так, як вони мають однаковий вихідний текст, але мають зовсім іншу поведінку через їх "приховане" закриття. Я не думаю, що код JavaScript дійсно може з'ясувати, чи має посилання на функцію закриття чи ні.
  • Якщо ви намагаєтеся зробити будь-які зміни в динамічному вихідному коді (наприклад: myFunction = Function(myFunction.toString().replace(/Hello/,'Hola'));), це не буде працювати, якщо myFunction це закриття (звичайно, ви навіть ніколи не думаєте про заміщення рядка вихідного коду під час виконання, але ...).
  • Можна отримувати декларації функцій в межах функціональних декларацій у межах функцій - і ви можете отримати закриття на більш ніж одному рівні.
  • Я думаю, що нормально закриття є терміном для функції, а також замінених змінних. Зауважте, що я не використовую це визначення в цій статті!
  • Я підозрюю, що закриття в JavaScript відрізняються від тих, що зазвичай знаходяться на функціональних мовах.

Посилання

Дякую

Якщо у вас є просто дізнався про закриття (тут чи в іншому місці!), тоді я зацікавлений в будь-якому зворотному зв'язку з вами про будь-які зміни, які ви могли б запропонувати, щоб зробити цю статтю більш зрозумілою. Надіслати електронною поштою morrisjohns.com (morris_closure @). Зауважте, що я не гуру на JavaScript - ні на закриттях.


Оригінальний пост Морріса можна знайти в Інтернет-архів.


6173



Блискучий Особливо люблю: "Закриття в JavaScript схоже на збереження копії всіх локальних змінних, як і тоді, коли вийшла функція". - e-satis
@ e-satis - як це може здатися, блискуче "копія всіх локальних змінних, як і тоді, коли вони вийшли з функції", є помилкою. Це припускає, що значення змінних копіюються, але насправді це набір самих змінних, які не змінюються після виклику функції (крім 'eval' можливо: blog.rakeshpai.me.2008/10/...) Це припускає, що функція повинна повернутися до створення закриття, але вона не повинна повернутися, перш ніж закриття можна використовувати як закриття. - dlaliberte
Це звучить добре: "Закриття в JavaScript схоже на збереження копії всіх локальних змінних, як і тоді, коли вийшла функція". Але це є помилкою з кількох причин. (1) Виклик функції не потрібно виходити, щоб створити закриття. (2) Це не копія цінності локальних змінних, але самі змінні. (3) Він не говорить, хто має доступ до цих змінних. - dlaliberte
Приклад 5 показує "gotcha", де код не працює, як передбачено. Але це не показує, як це виправити. Це інша відповідь показує спосіб зробити це. - Matt
Мені подобається, як починається цей пост з великими смілими літерами, що кажуть "Замикає не магія", і закінчує свій перший приклад: "Магія полягає в тому, що в JavaScript посилання функції також має таємне посилання на закриття, яке воно було створено". - Andrew Macheret


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

function foo(x) {
  var tmp = 3;

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

  bar(10);
}

foo(2);

Це завжди буде журналом 16, тому що bar може отримати доступ до x який був визначений як аргумент для foo, і він також може отримати доступ tmp від foo.

Це є закриття Функція не повинна повернутися для того, щоб бути названим закриттям. Просто доступ до змінних за межами вашої негайної лексичної області створює закриття.

function foo(x) {
  var tmp = 3;

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

var bar = foo(2); // bar is now a closure.
bar(10);

Вищезазначена функція буде також протоколювати 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, створюється новий контекст виконання. Разом з аргументами функції та батьківським об'єктом цей контекст виконання також приймає всі змінні, оголошені поза ним (у наведеному вище прикладі як "a", так і "b").

Можна створити більше, ніж одну функцію закриття, повернувши їх список або встановивши їх у глобальні змінні. Все це буде з посиланням на той же  x і те ж саме tmp, вони не створюють власних копій.

Ось номер x є буквальним числом. Як з іншими літералами в JavaScript, коли foo називається, число x є скопійовано в foo як його аргумент x.

З іншого боку, JavaScript завжди використовує посилання при роботі з об'єктами. Якщо скажеш, ти називав fooз об'єктом, закриття він поверне буде довідка той оригінальний об'єкт!

function foo(x) {
  var tmp = 3;

  return function (y) {
    console.log(x + y + tmp);
    x.memb = x.memb ? x.memb + 1 : 1;
    console.log(x.memb);
  }
}

var age = new Number(2);
var bar = foo(age); // bar is now a closure referencing age.
bar(10);

Як очікувалося, кожний дзвінок до bar(10) буде збільшуватися x.memb. Що не може бути очікуваним, це те x це просто посилання на той самий об'єкт, що й age змінна! Після пари дзвінків bar, age.memb буде 2! Це посилання є основою витоків пам'яті з HTML-об'єктами.


3808



@feeela: Так, кожна функція JS створює закриття. Перемінники, на які не посилаються, швидше за все стануть придатними для збирання сміття в сучасних двигунах JS, але це не змінює того факту, що при створенні контексту виконання в цьому контексті є посилання на контекст виконання виконання, його змінні та ця функція є об'єктом, який має потенціал для переміщення до іншої області змінної, зберігаючи при цьому оригінал посилання. Це закриття.
@Ali Я тільки що виявив, що наданий jsFiddle фактично нічого не підтверджує, оскільки delete не вдається. Тим не менш, лексичне середовище, яке функція буде виконувати як [[Scope]] (і в кінцевому підсумку використовувати як базу для власного лексичного середовища при виклику) визначається, коли виконується твердження, яке визначає функцію. Це означає, що функція є закриваючи весь вміст виконавчого поля, незалежно від того, які значення він насправді посилається, і чи він виходить із сфери застосування. Див. Розділи 13.2 та 10 в специфікація - Asad Saeeduddin
Це була хороша відповідь, поки вона не намагалася пояснити примітивні типи та посилання. Це призводить до того, що це абсолютно невірно, і розповідає про копіювання літералів, що нічого не має нічого спільного. - Ry-♦
Закриття - це відповідь JavaScript на кластеризоване, об'єктно-орієнтоване програмування. JS не залежить від класу, тому треба було знайти ще один спосіб реалізувати деякі речі, які не могли бути застосовані інакше. - Barth Zalewski
це повинна бути прийнятною відповіддю. Магія ніколи не відбувається у внутрішній функції. Це трапляється, якщо призначити зовнішню функцію змінній. Це створює новий контекст виконання для внутрішньої функції, тому "приватна змінна" може бути накопичена. Звичайно, оскільки змінна зовнішня функція призначена, вона може зберегти контекст. Перша відповідь просто зробить все складніше, не пояснюючи, що там дійсно відбувається. - Albert Gao


ПЕРЕДМОВА: ця відповідь була написана, коли було питання:

Як і колишній Альберт, він сказав: "Якщо ви не зможете пояснити це шестирічному віку, то ви самі це не зрозумієте". Добре, я намагався пояснити 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();

І навіть незважаючи на те, що дорослі знали справжніх принцес, вони ніколи не вірять єдинорогам чи драконам, тому що вони ніколи не зможуть їх побачити. Підлітки сказали, що вони існували лише у уяві маленької дівчини.

Але ми знаємо справжню правду; що маленька дівчина з принцесою всередині ...

... Це справді принцеса з маленькою дівчиною всередині.


2253



Я люблю це пояснення, справді. Для тих, хто читає та не слідує, аналогія така: функція princess () - це складна сфера, що містить особисті дані. За межами функції приватні дані неможливо переглянути або отримати доступ. Принцеса зберігає єдинорогів, драконів, пригод і т. Д. У своїй уяві (приватні дані), і дорослі не бачать їх для себе. АЛЕ княгині уяву захоплюється в закритті для story() функція, яка є єдиним інтерфейсом the littleGirl екземпляр розкриває світ магії. - Patrick M
Ось тут story це закриття, але код був var story = function() {}; return story; потім littleGirl буде закриттям. Принаймні, це враження від мене MDN використовує "приватні" методи з закриттями: "Ці три громадські функції є закриттям, які поділяють однакову ситуацію". - icc97
@ icc97, так story це закриття, що посилається на навколишнє середовище, надане в рамках princess. princess є ще й іншим мається на увазі закриття, тобто princess і littleGirl поділиться будь-яким посиланням на a parents масив, який існує ще в середовищі / області, де littleGirl існує і princess визначено. - Jacob Swartwood
@BenjaminKrupp Я додав явний кодовий коментар, щоб показати / припустити, що в рамках тіла є більше операцій princess ніж що написано. На жаль, ця історія зараз трохи недоречна в цій темі. Спочатку питання полягало в тому, щоб "пояснити закриття JavaScript для 5-річного старого"; моя відповідь була єдиною, яка навіть намагалася це зробити. Я не сумніваюся, що це було б невдало, але принаймні у такої відповіді, можливо, було б можливість зацікавити 5-річний старий. - Jacob Swartwood
Власне, мені це стало справжнім сенсом. І я повинен визнати, нарешті розуміючи закриття JS, використовуючи казки принцеса та пригоди, змушує відчувати себе дивним. - Crystallize


Якщо розглянути питання серйозно, ми повинні з'ясувати, що типовий 6-річний здатний пізнавати, хоча, звичайно, хтось, хто зацікавлений в 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();

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

Подальші пункти, що пояснюють, чому закриття цікаві:

  • Щоразу makeKitchen() називається, створюється нове закриття з власним окремо trashBags.
  • The trashBags змінна локальна до внутрішньої частини кожної кухні і не доступна назовні, але внутрішня функція на getTrashBagмайно має доступ до нього.
  • Кожне виклик функції створює закриття, але не потрібно буде тримати замикання навколо, якщо внутрішня функція, яка має доступ до внутрішньої частини затвора, не може бути викликана ззовні закриття. Повернення об'єкта за допомогою getTrashBag функція робить це тут.

693



Фактично, заплутано, функція makeKitchen дзвонити це фактичне закриття, а не кухонний об'єкт, який він повертає. - dlaliberte
Пройшовши крізь інші, я знайшов цю відповідь як найпростіший спосіб пояснити, що і чому закриття. - Chetabahana
Занадто багато меню та закуски, не вистачає м'яса та картоплі. Ви можете покращити цю відповідь лише одним коротким фразою, наприклад: "Закриття - це герметичний контекст функції, через відсутність будь-якого механізму визначення масштабу класів". - Jacob


Соломенний чоловік

Мені потрібно знати, скільки разів натискали кнопку і щось робити на кожному третьому кліку ...

Досить очевидне рішення

// 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

Отримати це? Отже, в нашому основному прикладі, змінна рахунку міститься в закритті і завжди доступна для обробника події, тому він зберігає свій стан від кліку до кліку.

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

Ось так; Ви зараз повністю інкапсулюєте цю поведінку.

Повний блог публікації (включаючи jQuery міркування)


527



Я не згоден з вашим визначенням, що таке закриття. Там немає причин, щоб він мав себе закликати. Це також трохи спрощено (і неточно), щоб сказати, що він повинен бути "повернутий" (багато обговорення на цьому в коментарях верхньої відповіді на це питання) - James Montagne
@ Джеймс, навіть якщо ви згодні, його приклад (і весь пост) є одним з найкращих, який я бачив. Поки питання не старий і вирішено для мене, він цілком заслуговує +1. - e-satis
"Мені потрібно знати, скільки разів натискали кнопку, і щось робити на кожному третьому кліку ..." Це привернуло мою увагу. Приклад використання та рішення показують, як закриття не така загадкова річ, і що багато з нас написали їх, але не точно знали офіційне ім'я. - Chris22
Красивий приклад, оскільки він показує, що "count" у другому прикладі зберігає значення "count", а не скидання до 0 при кожному натисканні на "елемент". Дуже інформативний! - Adam
+1 для закриття поведінки. Чи можемо ми обмежити? закриття поведінки до функції в JavaScript або ця концепція може бути застосована і до інших структур мови? - Dziamid


Замикання важко пояснити, тому що вони використовуються для виконання певної поведінки, яку всі інтуїтивно очікують від роботи. Я знаходжу найкращий спосіб пояснити їх (і як це Я дізналися, що вони роблять) - це уявити ситуацію без них:

    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.


445



Я згоден. Надання функцій значущим іменам замість традиційних "foobar" також допомагає мені багато чого. Семантика розраховує. - Ishmael
так що в псевдо-мові це в основному схоже alert(x+3, where x = 5). The where x = 5це закриття. Маю рацію? - Jus12
@ Jus12: точно. За сценою, закриття - це просто місце, де зберігаються поточні змінні значення ("прив'язки"), як у вашому прикладі. - Konrad Rudolph
Це саме такий приклад, який збиває багато людей у ​​тому, що це таке цінності які використовуються у поверненій функції, а не змінюваної змінної сама. Якщо його було змінено на "return x + = y", то краще як для тієї, так і для іншої функції "x * = y", то було б зрозуміло, що нічого не буде скопійовано. Для людей, котрі використовують для складання кадрів, уявіть собі замість використання куча кадрів, які можуть продовжувати існувати після повернення функції. - Matt
@ Матт я не згоден. Прикладом є ні Передбачається вичерпно документувати всі властивості. Це мається на увазі редуктивний і ілюструють виняткову особливість концепції. О.П. попросив просте пояснення ("для шестирічного"). Візьміть прийняту відповідь: це абсолютно не вдається надає коротке пояснення, саме тому, що він намагається бути вичерпним. (Я згоден з вами, що це важлива властивість JavaScript, що зв'язування відбувається за посиланням, а не за значенням ... але знову ж таки успішне пояснення є тим, яке зводиться до мінімуму.) - Konrad Rudolph


Це спроба розібрати декілька (можливих) непорозумінь щодо закриття, які з'являються в деяких інших відповідях.

  • Закриття не тільки створюється, коли ви повертаєте внутрішню функцію. Фактично, що входить в функцію не потрібно взагалі повертатися щоб його було закрито. Ви, натомість, можете призначити вашу внутрішню функцію змінній у зовнішній області або передавати її як аргумент іншої функції, де вона може бути викликана негайно або через деякий час. Отже, можливо, створена замикання функції, що закриває як тільки викличе функцію додавання оскільки будь-яка внутрішня функція має доступ до цього замикання кожного разу, коли викликається внутрішня функція, до або після повернення функції, яка об'єднує.
  • Закриття не стосується копії старі цінності змінних у своїй сфері. Самі змінні є частиною закриття, і тому значення, яке видно при доступі до однієї з цих змінних, є останньою цінністю під час доступу. Ось чому внутрішні функції, створені всередині циклів, можуть бути складними, оскільки кожен має доступ до тих самих зовнішніх змінних, а не захоплення копії змінних під час створення або виклику функції.
  • "Змінні" в закритті включають будь-які називані функції оголошено в рамках функції. Вони також містять аргументи функції. Закриття також має доступ до його змінних, що містять закриття, до рівня глобального масштабу.
  • Закриття використовують пам'ять, але вони не викликають витоків пам'яті оскільки сам JavaScript самостійно очищає власні кругові структури, на які не вказано посилання. Витоки пам'яті Internet Explorer, що містять закриття, створюються, коли не вдається від'єднати значення атрибута DOM, які посилаються на закриття, таким чином зберігаючи посилання на, можливо, кругові структури.

343



До речі, я додав цю "відповідь" з уточненнями, щоб безпосередньо не розглянути первинне питання. Натомість я сподіваюсь, що будь-яка проста відповідь (для 6-річного віку) не вводить неправильні уявлення про цей складний предмет. Наприклад, Популярна вікі-відповідь вище говорить: "Закриття це, коли ви повертаєте внутрішню функцію". Окрім граматичного помилки, це технічно неправильне. - dlaliberte
Джеймс, я сказав, що закриття є "ймовірно" створене під час виклику функції, що додають, тому що це правдоподібно, що реалізація може відкласти створення закриття до деякого часу пізніше, коли вона вирішить, що закриття є абсолютно необхідним. Якщо внутрішня функція, визначена в функції, що не входить, відсутня, то не потрібно буде закривати. Таким чином, можливо, він може чекати, доки не буде створено першу внутрішню функцію, щоб потім створити закриття з контексту виклику функції, що належить. - dlaliberte
@ Beetro-Beetroot Припустимо, що у нас є внутрішня функція, яка передається іншої функції, де вона використовується раніше зовнішня функція повертається, і припустимо, що ми також повертаємо ту саму внутрішню функцію з зовнішньої функції. У обох випадках це однакова функція, але ви кажете, що до повернення зовнішньої функції внутрішня функція "прив'язана" до стеків викликів, тоді як після повернення, внутрішня функція раптово зв'язана з закриттям. В обох випадках вона поводиться однаково; семантика ідентична, так що ви не просто говорите про деталі реалізації? - dlaliberte
@ Буряк-Буряк, дякуємо за ваш відгук, і я радий, що я вас роздумував. Я все ще не бачу ніякої семантичної різниці між живим контекстом зовнішньої функції та тим самим контекстом, коли він стає закриттям, як функція повертає (якщо я розумію ваше визначення). Внутрішня функція не хвилює. Збирання сміття не підходить, оскільки внутрішня функція зберігає посилання на контекст / закриття в будь-якому випадку, а виклик зовнішньої функції просто втрачає свою посилання на контекст дзвінка. Але це незрозуміло для людей, і, можливо, краще просто назвати це контекстом дзвінка. - dlaliberte
Ця стаття важко прочитати, але я думаю, що це насправді підтримує те, що я говорю. У ньому говориться: "Закриття утворюється шляхом повернення об'єкта функції [...] або безпосереднім присвоєнням посилання на такий об'єкт функції, наприклад, глобальної змінної". Я не маю на увазі, що GC не має значення. Скоріше, через GC і тому, що внутрішня функція приєднується до контексту виклику зовнішньої функції (або [[scope]], як це говорить стаття), то не має значення, повертається виклик зовнішньої функції, оскільки це зв'язування з внутрішнім функція є важливою річчю. - dlaliberte


ОК, 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");

331



Добре грав і відповідає оригінальному плакатові. Я думаю, що це найкраща відповідь. Я збираюся використати багаж так само: уявіть, що ви йдете у будинок бабусі, і ви пакуєте свій футляр Nintendo DS з ігровими картками всередині вашої футляра, а потім упаковуєте футляр у вашому рюкзаку, а також поставляєте ігрові карти в кишенях для рюкзаків, і Тоді ви поставите все це в великому валізі з більшістю ігрових карт в кишенях валізи. Коли ви потрапляєте до будинку бабусі, ви можете грати будь-яку гру на вашому DS, якщо всі зовнішні справи є відкритими. або щось для цього. - slartibartfast


А. закриття дуже схожий на об'єкт. Вона отримує екземпляр, коли ви називаєте функцію.

Сфера застосування a закриття в JavaScript є лексичним, що означає, що все, що міститься в рамках функції закриття належить, має доступ до будь-якої змінної, яка знаходиться в ній.

Змінні містяться в закриття якщо ви

  1. призначити його var foo=1; або
  2. просто напиши var foo;

Якщо внутрішня функція (функція, що міститься всередині іншої функції), звертається до такої змінної, не визначивши її у своїй області з var, змінює зміст змінної на зовнішній закриття.

А. закриття переживає час виконання функції, яка її породила. Якщо інші функції виводять його з закриття / сфера застосуванняв якій вони визначені (наприклад, як повернені значення), вони продовжують вказувати на це закриття.

Приклад

 function example(closure) {
   // define somevariable to live in the closure of example
   var somevariable = 'unchanged';

   return {
     change_to: function(value) {
       somevariable = value;
     },
     log: function(value) {
       console.log('somevariable of closure %s is: %s',
         closure, somevariable);
     }
   }
 }

 closure_one = example('one');
 closure_two = example('two');

 closure_one.log();
 closure_two.log();
 closure_one.change_to('some new value');
 closure_one.log();
 closure_two.log();

Вихідні дані

somevariable of closure one is: unchanged
somevariable of closure two is: unchanged
somevariable of closure one is: some new value
somevariable of closure two is: unchanged

320



Ого, ніколи не знав, що ви можете використовувати рядки заміни в console.log так. Якщо хтось інший зацікавлений, є більше: developer.mozilla.org/en-US/docs/DOM/... - Flash
Змінні, що знаходяться в списку параметрів функції, також є частиною закриття (наприклад, не обмежуючись лише var) - Thomas Eding