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


7636

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

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


391
Моя проблема цих і багатьох відповідей полягає в тому, що вони підходять до нього з абстрактної, теоретичної точки зору, а не починаючи з пояснення просто, чому в Javascript потрібні закриття та практичні ситуації, в яких ви їх використовуєте. Ви закінчуєте статтю «tl; dr», яку вам доводиться переглядати, весь час думаючи, «але чому?». Я б просто почав із: закриття - це акуратний спосіб вирішення наступних двох реалій JavaScript: a. сфера дії знаходиться на рівні функції, а не на рівні блоку, а b. багато з того, що ви робите на практиці в JavaScript, асинхронно / залежно від подій.
Джеремі Бертон

53
@Redsandro Для одного це полегшує написання керованого подіями коду. Я можу запустити функцію, коли сторінка завантажується, щоб визначити особливості HTML або доступних функцій. Я можу визначити та встановити обробник у цій функції і мати всю цю інформацію про контекст, доступну щоразу, коли виклик обробника не потребує його повторного запиту. Вирішіть проблему один раз, використовуйте повторно на кожній сторінці, де потрібен цей обробник із зменшеними накладними витратами на повторне виклик обробника. Ви коли-небудь бачите, щоб одні і ті ж дані двічі переосмислювалися мовою, яка їх не має? Закриття набагато простіше уникнути подібних речей.
Ерік Реппен

1
@Erik Reppen дякую за відповідь. Насправді мені було цікаво переваги цього важко читаемого closureкоду, на відміну від того, Object Literalщо він повторно використовує та зменшує накладні витрати так само, але вимагає на 100% менше коду для обгортання.
Редсандро

6
Для програмістів Java коротка відповідь полягає в тому, що це еквівалент функції внутрішнього класу. Внутрішній клас також містить неявний покажчик на екземпляр зовнішнього класу і використовується для майже тієї ж мети (тобто створення обробників подій).
Борис ван Шкотен

8
Я вважаю цей практичний приклад дуже корисним: youtube.com/watch?v=w1s9PgtEoJs
Abhi

Відповіді:


7357

Закриття є сполученням:

  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, створюється закриття.
  • Повернення a functionзсередини іншої функції є класичним прикладом закриття, оскільки стан всередині зовнішньої функції неявно доступний повернутій внутрішній функції, навіть після того, як зовнішня функція завершила виконання.
  • Щоразу, коли ви використовуєте eval()всередині функції, використовується закриття. Текст, на який evalможна посилатися на локальні змінні функції, а в не строгому режимі ви навіть можете створювати нові локальні змінні, використовуючиeval('var foo = …') .
  • Коли ви використовуєте new Function(…)( конструктор функції ) всередині функції, вона не закривається над своїм лексичним середовищем: замість цього закривається над глобальним контекстом. Нова функція не може посилатися на локальні змінні зовнішньої функції.
  • Закриття в JavaScript - це як збереження посилання ( НЕ копія) на область в точці декларації функції, яка, в свою чергу, зберігає посилання на її зовнішню область застосування тощо, аж до глобального об'єкта вгорі сфера застосування.
  • Закриття створюється при оголошенні функції; це закриття використовується для налаштування контексту виконання, коли функція викликається.
  • Новий набір локальних змінних створюється кожного разу при виклику функції.

Посилання


74
Це звучить приємно: "Закриття JavaScript - це схоже на збереження копії всіх локальних змінних, подібно до того, як вони були при виході функції". Але це вводить в оману з кількох причин. (1) Виклик функції не повинен виходити, щоб створити закриття. (2) Це не копія значень локальних змінних, а самих змінних. (3) Не вказується, хто має доступ до цих змінних.
dlaliberte

27
У прикладі 5 показано "gotcha", де код не працює за призначенням. Але це не показує, як це виправити. Ця інша відповідь показує спосіб це зробити.
Метт

190
Мені подобається, як цей пост починається великими жирними літерами, що говорять "Закриття не магія", і закінчує свій перший приклад "Магія полягає в тому, що в JavaScript посилання на функцію також має таємне посилання на закриття, в якому було створено".
Ендрю Машерет

6
Приклад №3 - це змішування закриттів із підйомними текстами. Тепер я думаю, що пояснити лише закриття досить складно, не втручаючись у підйомну поведінку. Це мені найбільше допомогло: 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()виводить друк того, що ви хочете спочатку.
Нір

3988

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

@Ali Я щойно виявив, що наданий jsFiddle насправді нічого не підтверджує, оскільки deleteне працює. Тим не менш, лексичне середовище, яке функція буде виконувати як [[Область]] (і в кінцевому підсумку використовувати як базу для власного лексичного середовища при виклику) визначається при виконанні оператора, що визначає функцію. Це означає , що функція є закриття над вмістом ЦІЛИХ виконуючого обсягу, незалежно від того, які значення він на насправді ставиться до і уникає сфери чи. Перегляньте розділи 13.2 та 10 у специфікації
Асад Саєдюддін

8
Це була гарна відповідь, поки не спробували пояснити примітивні типи та посилання. Це стає абсолютно неправильним і говорить про копіювання літералів, що насправді нічого не має.
Ry-

12
Закриття - це відповідь JavaScript на класному, об’єктно-орієнтованому програмуванні. JS не базується на класах, тому треба було знайти інший спосіб реалізувати деякі речі, які не могли бути реалізовані інакше.
Bartłomiej Zalewski

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();

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

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

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


340
Я люблю це пояснення, по-справжньому. Для тих, хто читає його і не дотримується, аналогія така: функція princecess () - це складна область, що містить приватні дані. Поза межами функції приватні дані не можна побачити або отримати доступ до них. Принцеса зберігає у своїй уяві єдиноріг, драконів, пригод тощо, і дорослі не можуть їх бачити самі. АЛЕ уява принцеси захоплена 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-річного віку"; моя відповідь була єдиною, хто навіть намагався це зробити. Я не сумніваюся, що це не вдалося б сумно зазнати невдач, але, принаймні, ця відповідь могла б мати шанс взяти участь у старі 5 років.
Jacob Swartwood

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функції робить це тут.

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. Така поведінка дозволяє будь-якій функції мати доступ до тієї сфери, в якій вона створена, на невизначений термін.Щоб практично застосувати це, я негайно викликаю функцію, яка повертає іншу функцію, і оскільки функція, яку я повертаю, має доступ до внутрішньої змінної підрахунку (через поведінку закриття, поясненої вище), це призводить до приватного використання для використання результатом функція ... Не так просто? Давайте розведемо його вниз ...

Просте однолінійне закриття

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


11
Я не згоден з вашим визначенням того, що таке закриття. Немає причин, щоб це було самозванним. Це також трохи спрощено (і неточно), щоб сказати, що його потрібно "повернути" (багато дискусій з цього приводу в коментарях верхньої відповіді на це питання)
Джеймс Монтань

40
@James, навіть якщо ви не погоджуєтесь, його приклад (і весь пост) - один із найкращих, що я бачив. Хоча питання для мене не старе і вирішено, воно цілком заслуговує +1.
e-satis

84
"Мені потрібно знати, скільки разів була натиснута кнопка, і робити щось на кожному третьому натисканні ..." ЦЕ мою увагу привернуло. Випадок використання та рішення, що показує, як закриття не є такою загадковою річчю, і що багато з нас писали їх, але точно не знали офіційної назви.
Chris22

Хороший приклад, оскільки він показує, що "count" у 2-му прикладі зберігає значення "count" і не скидається на 0 щоразу, коли натискається "елемент". Дуже інформативно!
Адам

+1 за поведінку щодо закриття . Чи можна обмежити поведінку закриття для функцій в JavaScript або ця концепція може бути також застосована до інших структур мови?
Дзямід

492

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

    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
@Matt Я не згоден. Приклад не повинен вичерпно документувати всі властивості. Він повинен бути відновним і ілюструвати помітну особливість поняття. ОП попросила просте пояснення («для шестирічного віку»). Візьміть прийняту відповідь: Це абсолютно не дає чіткого пояснення саме тому, що воно намагається бути вичерпним. (Я згоден з вами, що важливою властивістю JavaScript є те, що прив'язка є посиланням, а не значенням ... але знову ж таки, вдале пояснення - це те, що зводиться до мінімуму.)
Конрад Рудольф,

@KonradRudolph Мені подобається стиль та стислість вашого прикладу. Я просто рекомендую трохи змінити його, щоб остаточна частина "Єдине рішення ..." стала справжньою. В даний час існує насправді інше, більш просте рішення для вашого сценарію, що не НЕ відповідає яваскрипт продовжень, і робить відповідають поширеній помилці про те , що продовження є. Таким чином приклад в його нинішньому вигляді є небезпечним. Це не пов'язане з вичерпним переліченням властивостей, це пов'язано з розумінням того, що x є у поверненій функції, що зрештою є головним моментом.
Метт

@Matt Хм, я не впевнений, що вас цілком розумію, але я починаю бачити, що у вас може бути дійсна точка. Оскільки коментарі занадто короткі, чи можете ви пояснити, що ви маєте на увазі в суті / пасті або в чаті? Дякую.
Конрад Рудольф

2
@KonradRudolph Я думаю, мені не було зрозуміло призначення x + = y. Метою було лише показати, що повторні виклики повернутої функції продовжують використовувати ту саму змінну x (на відміну від того самого значення , яке люди уявляють, що "вставляється" під час створення функції). Це як перші два сповіщення у вашій загадці. Метою додаткової функції x * = y було б показати, що декілька повернених функцій всі ділять один і той же x.
Метт

376

Гаразд, 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, доки відкриті всі зовнішні корпуси. або щось для цього.
slartibartfast

376

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

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

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


56
Нічого собі, ніколи не знав, що ти можеш використовувати підстановки рядків у console.logподібному. Якщо когось цікавить, є ще: developer.mozilla.org/en-US/docs/DOM/…
Flash

7
Змінні, що знаходяться у списку параметрів функції, також є частиною закриття (наприклад, не обмежуються лише var).
Томас Едінг

Замикання звучать більше як предмети та класи тощо. Не впевнений, чому багато людей не порівнюють цих двох - нам, новачкам, буде легше вчитися!
almaruf

365

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

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

15
Джеймс, я сказав, що закриття "ймовірно" створене під час виклику додаткової функції, оскільки правдоподібно, що реалізація може відкласти створення закриття до десь пізніше, коли він вирішить, що закриття абсолютно необхідне. Якщо в додатковій функції не визначена внутрішня функція, закриття не потрібно. Тож, можливо, це може зачекати, поки не буде створена перша внутрішня функція, а потім створити закриття з контексту виклику вкладеної функції.
dlaliberte

9
@ Beetroot-Beetroot Припустимо, у нас є внутрішня функція, яка передається іншій функції, де вона використовується до повернення зовнішньої функції, і припустимо, ми повертаємо ту саму внутрішню функцію із зовнішньої функції. Це ідентично однакова функція в обох випадках, але ви говорите, що перед поверненням зовнішньої функції внутрішня функція "прив'язується" до стека виклику, тоді як після повернення внутрішня функція раптово пов'язана із закриттям. Він поводиться однаково в обох випадках; семантика однакова, тож ви не просто говорите про деталі реалізації?
dlaliberte

7
@ Beetroot-Beetroot, дякую за відгуки, і я радий, що ти задумався. Я все ще не бачу жодної смислової різниці між живим контекстом зовнішньої функції та тим самим контекстом, коли вона стає закритою, коли функція повертається (якщо я розумію ваше визначення). Внутрішня функція не хвилює. Збір сміття не турбується, оскільки внутрішня функція підтримує посилання на контекст / закриття в будь-якому випадку, а абонент зовнішньої функції просто скидає своє посилання на контекст виклику. Але це бентежить людей, і, можливо, краще просто назвати це контекстом виклику.
dlaliberte

9
Цю статтю важко читати, але я думаю, що вона насправді підтримує те, що я говорю. У ній сказано: "Замикання утворюється шляхом повернення об'єкта функції [...] або безпосередньо присвоєнням посилання на такий об'єкт функції, наприклад, глобальній змінній." Я не маю на увазі, що GC не має значення. Скоріше, через GC і тому, що внутрішня функція приєднана до контексту виклику зовнішньої функції (або [[область]], як сказано в статті), то не має значення, чи повертається виклик зовнішньої функції, тому що зв'язування з внутрішнім функція - важлива річ.
dlaliberte

3
Чудова відповідь! Одне, що вам слід додати, - це те, що всі функції закриваються над усім вмістом виконавчої області, в якій вони визначені. Не має значення, посилаються вони на якусь або жодну із змінних з батьківської області: посилання на лексичне середовище батьківської області зберігається як [[Область]] беззастережно. Це видно з розділу про створення функцій у специфікації ECMA.
Asad Saeeduddin

236

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

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

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

Повна публікація:

То що ж таке про закриття?


Тож чи можна на цьому прикладі підкреслити основну користь закриття? Скажіть, у мене є функція emailError (sendToAddress, errorString), я можу потім сказати, devError = emailError("devinrhode2@googmail.com", errorString)а потім мати власну власну версію спільної функції emailError?
Devin G Rhode

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

Внутрішні функції, такі як addі 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

Як би я пояснив це шестирічному віку:

Ви знаєте, як дорослі люди можуть володіти будинком, і вони називають це домом? Коли у мами є дитина, дитина насправді нічого не має, правда? Але батьки володіють будинком, тож коли хтось запитує дитину «Де твій дім?», Він / вона може відповісти «той будинок!» Та вказати на будинок батьків. "Закриття" - це здатність дитини завжди (навіть якщо вона знаходиться за кордоном) мати змогу сказати, що в неї є дім, навіть якщо власник будинку справді є батьком.


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 #


11
Код є "правильним", як приклад закриття, навіть якщо він не стосується частини коментаря щодо використання закриття після повернення externalFunction. Тож це не чудовий приклад. Існує багато інших способів закриття, які не передбачають повернення внутрішньої функції. наприклад, InternalFunction може бути переданий іншій функції, де вона викликається негайно або зберігається і викликається деякий час пізніше, і у всіх випадках вона має доступ до контексту externalFunction, який був створений при його виклику.
dlaliberte

6
@syockit Ні, Мосс помиляється. Замикання створюється незалежно від того, чи функція коли-небудь уникає сфери, в якій вона визначена, і безумовно створене посилання на батьківське лексичне середовище робить усі змінні в батьківській області доступними для всіх функцій, незалежно від того, викликаються вони зовні або всередині сфера, в якій вони були створені.
Асад Саедюддін

176

Я схильний вчитися краще завдяки порівнянню GOOD / BAD. Мені подобається бачити робочий код, за яким слідує непрацюючий код, з яким хтось може зіткнутися. Я зібрав 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змінну SAME з батьківської області, де вона була визначена замість 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


1
Гарне доповнення, дякую. Щоб зробити це більш зрозумілим, можна уявити, як створюється масив "поганий" у циклі "поганий" з кожною ітерацією: 1-а ітерація: [функція () {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виправлення різниці.
Rupam Datta

Чи не тут "Закриття зроблено правильно" є прикладом "закриття всередині закриття"?
TechnicalSmile

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

Якщо ви просто хочете зберегти результат в масиві в першій ітерації , то ви можете вбудувати це наступним чином : arr[index] = (function (n) { return 'n = ' + n; })(index);. Але тоді ви зберігаєте отриманий рядок у масиві, а не функцію для виклику, яка перемагає крапку мого прикладу.
Шев

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
}

Кожен раз, коли ви її називаєте, створюється локальна змінна "name" і надається ім'я "Mary". І кожного разу, коли функція виходить із змінної, вона втрачається, а ім'я - це забуте.

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

Але в 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
Це пояснення, яке мало для мене сенс, оскільки воно не передбачає значних попередніх знань технічних термінів. Тут пояснення, що голосує під головою, передбачає, що людина, яка не розуміє закриття, має повне і повне розуміння таких термінів, як "лексична сфера дії" та "контекст виконання" - хоча я можу зрозуміти це концептуально, я не думаю, що я як мені зручні деталі про них, як я маю бути, і пояснення, в якому зовсім немає жаргону, - це те, що змусив закриття нарешті натиснути на мене, дякую. Як бонус, я думаю, що це також пояснює, яка сфера дії є дуже стислою.
Емма Ш

103

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

Ось закриття:

var a = 42;

function b() { return a; }

Так. Напевно, ви використовуєте це багато разів на день.


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

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


5
Ця відповідь, мабуть, не допоможе відплутати людей. Приблизним еквівалентом у традиційній мові програмування може бути створення b () як методу на об'єкті, який також має приватну константу або властивість a. На мій погляд, несподіванкою є те, що об’єкт області JS ефективно забезпечує aяк властивість, а не константу. І ви помітите таку важливу поведінку, лише якщо ви модифікуєте її, як вreturn a++;
Джон Кумбс

1
Саме так сказав Джон. Перш ніж я нарешті заграв закриття, мені важко було знайти практичні приклади. Так, флорибон створив замикання, але якби неосвічений мене це не навчило б мене абсолютно нічого.
Шев

3
Це не визначає, що таке закриття - це лише приклад, який використовує таке. І це не стосується нюансу того, що відбувається, коли область закінчується; Я не думаю, що у когось виникло питання щодо лексичного обстеження, коли всі сфери дії все ще існують, і особливо у випадку глобальної змінної.
Джерард ONeill

91

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

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

var i;
function foo(x) {
    var tmp = 3;
    i = function (y) {
        console.log(x + y + (++tmp));
    }
}
foo(2);
i(3);

Невелике роз’яснення щодо можливої ​​неоднозначності. Коли я сказав "Насправді, функція, що вкладається, взагалі не потребує повернення". Я не мав на увазі "повернути значення", але "все ще активний". Отже, приклад не показує цього аспекту, хоча він показує інший спосіб внутрішньої функції можна передати у зовнішню область. Основний пункт, який я намагався зробити, - це про час створення закриття (для функції, що вкладається), оскільки деякі люди, здається, думають, що це відбувається, коли повертається функція, що закриває. Інший приклад повинен показати , що замикання створюється , коли функція називається .
dlaliberte

88

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


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

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.");

80

Автор Закриття досить добре пояснив закриття, пояснивши причину, чому вони нам потрібні, а також пояснив LexicalEl Environment, який необхідний для розуміння закриття.
Ось підсумок:

Що робити, якщо доступ до змінної, але це не локально? Як тут:

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

У цьому випадку інтерпретатор знаходить змінну у зовнішньому LexicalEnvironmentоб'єкті.

Процес складається з двох етапів:

  1. По-перше, коли створюється функція f, вона не створюється в порожньому просторі. Існує поточний об'єкт LexicalEl Environment. У верхньому випадку це вікно (a не визначено на момент створення функції).

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

Коли функція створена, вона отримує приховане властивість, назване [[Область]], яке посилається на поточну LexicalEn Environment.

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

Якщо змінну читають, але її не можна знайти ніде, генерується помилка.

Вкладені функції

Функції можуть бути вкладені одна в іншу, утворюючи ланцюжок LexicalEl Environment, який також можна назвати ланцюгом області.

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

Отже, функція g має доступ до g, a і f.

Закриття

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

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

Позначення лексичних середовищ:

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

Як ми бачимо, this.sayце властивість у користувальницькому об’єкті, тому воно продовжує жити після завершення роботи користувача.

І якщо ви пам’ятаєте, коли this.sayвін створений, він (як і кожна функція) отримує внутрішнє посилання this.say.[[Scope]]на поточну LexicalEn Environment. Отже, LexicalEn Environment з поточного виконання користувача залишається в пам'яті. Усі змінні Користувача також є його властивостями, тому вони також ретельно зберігаються, а не перетворюються як зазвичай.

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

Узагальнити:

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

Це називається закриттям.


78

Функції JavaScript можуть отримати доступ до своїх:

  1. Аргументи
  2. Місцеві організації (тобто їх локальні змінні та локальні функції)
  3. Навколишнє середовище, що включає:
    • глобальних, включаючи DOM
    • що-небудь у зовнішніх функціях

Якщо функція отримує доступ до свого оточення, то функція є закриттям.

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

Приклад закриття, що використовує глобальне середовище:

Уявіть, що події кнопки "Переповнення стека" та "Голосування вниз" реалізуються як закриття, "голосUp_click" та "голосовання", які мають доступ до зовнішніх змінних - це "VoVoTUp" та "Повторнезавдання", які визначені у всьому світі. (Для простоти я маю на увазі кнопки StackOverflow «Питання для голосування», а не масив кнопок «Відповісти голосування»).

Коли користувач натискає кнопку VoteUp, функція voiceUp_click перевіряє, чи є "VoVoDDown" == "істиною", щоб визначити, чи потрібно голосувати за або просто скасувати голосування за відмову. Функція voiceUp_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: "Брекети", які виглядають так, {}- це тоновані вікна нашої фабрики. Зсередини фабрики ви можете бачити, а зовні не видно.

ПРИКЛАД ДОГОВОРНОГО КОДУ

Наш код починається зі слова функції , тому ми знаємо , що це один! Тоді назва функції sing - це мій власний опис того, про яку функцію йдеться. Потім дужки () . Дужки завжди є для функції. Іноді вони порожні, і іноді у них є що - то в цьому один має слово .: (person). Після цього є такий брекет {. Це означає початок функції sing () . У нього є партнер, який позначає кінець sing (), як це}

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

Тож ця функція може мати щось спільне із співом, і може знадобитися певна інформація про людину. Всередині є вказівки робити щось із цими даними.

Тепер, після функції sing () , в кінці коду знаходиться рядок

var person="an old lady";

ЗМІННА: Літери 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, щоб пограти з ним.


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 можна вважати закриттям.

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

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

Також слід розуміти, що локальні змінні в 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();

47

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

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

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

outerFunction();

ALERT: мавпа

У наведеному вище прикладі називається зовнішняФункція, яка в свою чергу називає внутрішнюФункцію. Зверніть увагу на те, як зовнішнійVar доступний для InternalFunction, про що свідчить його правильне повідомлення про значення externalVar.

Тепер розглянемо наступне:

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

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

ALERT: мавпа

referenceToInnerFunction встановлений у externalFunction (), який просто повертає посилання на innerFunction. Коли викликається referenceToInnerFunction, він повертає externalVar. Знову, як зазначено вище, це демонструє, що InternalFunction має доступ до externalVar, змінної externalFunction. Крім того, цікаво зазначити, що він зберігає цей доступ навіть після того, як зовнішнє виконання закінчило виконання.

І ось де речі стають по-справжньому цікавими. Якщо ми повинні позбутися зовнішньої функції, скажімо, встановити її на нуль, ви можете подумати, що 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, полягає в тому, що коли закриття вперше було створене шляхом розміщення InternalFunction всередині externalFunction, innerFunction додало посилання на область externalFunction (її змінні та функції) до її ланцюга областей. Це означає, що innerFunction має вказівник або посилання на всі змінні зовнішньої функції, включаючи externalVar. Так що навіть коли externalFunction закінчив виконання, або навіть якщо його видалено або встановлено на нульове значення, змінні в його області, як і зовнішнійVar, залишаються в пам'яті через видатне посилання на них з боку внутрішньогоFunction, до якого було повернуто referenceToInnerFunction. Щоб справді вивільнити зовнішню пам’ять externalVar та решту змінних externalFunction, ви повинні позбутися цього видатного посилання на них,

//////////

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

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

    innerFunction();
}

outerFunction();

АЛЕРТ: горила

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


45

Я просто вкажу їх на сторінку Mozilla Closures . Це найкраще, найбільш стисле і просте пояснення основ закриття та практичного використання, які я знайшов. Настійно рекомендується всім, хто вивчає JavaScript.

І так, я б навіть рекомендував це 6-річному віку - якщо 6-річний чоловік дізнається про закриття, то логічно, що вони готові зрозуміти стисле і просте пояснення, подане в статті.


Я погоджуюся: згадана сторінка Mozilla особливо проста і стисла. Дивно, але ваш пост не був настільки високо оцінений, як інші.
Бріс Кустільяс,
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.