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


562

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

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

Неправильне рішення без закриттів або нічого подібного до закриття було б:

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

що, звичайно, буде надрукувати в 10 разів більше значення iциклу після циклу, а саме 10.

Тож його спроба була:

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

друк від 0 до 9, як очікувалося.

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

Моя спроба:

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

Тож я захоплюю i(названий i2під час закриття), але тепер повертаю ще одну функцію і передаю це навколо. У моєму випадку функція, передана setTimeout, дійсно захоплює i.

Тепер хто використовує закриття, а хто ні?

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



1
@leemes: Перегляньте мою редакцію ніндзя для другого посилання.
Блендер

2
ми щойно уклали угоду: той, хто має право, отримає бали, пов'язані з цим питанням
brilout

1
@leemes - Ви обидва використовуєте закриття. Ви обоє зробили дві функції - зовнішню функцію та внутрішню; і обидві ваші внутрішні функції - це закриття. Усі ваші функції - лямбда ( анонімні функції ). Прочитайте мою відповідь для деталей.
Аадіт М Шах

1
@blesh - я поняття не маю, що таке модифіковане закриття. Я бачу, що ваше посилання вказує на код C #. Чи підтримуються модифіковані закриття JavaScript?
Аадіт М Шах

Відповіді:


650

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

Просте пояснення закриття:

  1. Візьміть функцію. Назвемо це F.
  2. Перерахуйте всі змінні F.
  3. Змінні можуть бути двох типів:
    1. Локальні змінні (зв'язані змінні)
    2. Не локальні змінні (вільні змінні)
  4. Якщо F не має вільних змінних, це не може бути закриттям.
  5. Якщо F має якісь - або вільні змінні (які визначені в вигляді батьківського обсягу F) , то:
    1. Там повинно бути тільки один з батьків обсяг F , до якого вільна змінна пов'язана.
    2. Якщо на F посилається поза межами цієї батьківської області, то це стає закриттям для цієї вільної змінної.
    3. Ця вільна змінна називається підвищеною величиною закриття F.

Тепер давайте скористаємося цим, щоб з’ясувати, хто використовує закриття, а хто ні (для пояснення я назвав функції):

Випадок 1: Програма вашого друга

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

У вищевказаній програмі є дві функції: fі g. Давайте подивимось, чи є вони закриттями:

Для f:

  1. Перерахуйте змінні:
    1. i2- локальна змінна.
    2. iє вільною змінною.
    3. setTimeoutє вільною змінною.
    4. g- локальна змінна.
    5. consoleє вільною змінною.
  2. Знайдіть батьківську область, до якої прив’язана кожна вільна змінна:
    1. iбуде пов'язаний з глобальною області.
    2. setTimeoutбуде пов'язаний з глобальною області.
    3. consoleбуде пов'язаний з глобальною області.
  3. У якій області посилається функція ? Глобальний масштаб .
    1. Тому iНЕ зімкнулися на f.
    2. Тому setTimeoutНЕ зімкнулися на f.
    3. Тому consoleНЕ зімкнулися на f.

Таким чином, функція fне є закриттям.

Для g:

  1. Перерахуйте змінні:
    1. consoleє вільною змінною.
    2. i2є вільною змінною.
  2. Знайдіть батьківську область, до якої прив’язана кожна вільна змінна:
    1. consoleбуде пов'язаний з глобальною області.
    2. i2буде пов'язаний з областю f.
  3. У якій області посилається функція ? ОбсягsetTimeout .
    1. Тому consoleНЕ зімкнулися на g.
    2. Отже , i2буде закрита в протягом від g.

Таким чином, функція gє закриттям для вільної змінної i2(що є надмірним значенням g), коли на неї посилається зсередини setTimeout.

Погане для вас: ваш друг використовує закриття. Внутрішня функція - це закриття.

Випадок 2: Ваша програма

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

У вищевказаній програмі є дві функції: fі g. Давайте подивимось, чи є вони закриттями:

Для f:

  1. Перерахуйте змінні:
    1. i2- локальна змінна.
    2. g- локальна змінна.
    3. consoleє вільною змінною.
  2. Знайдіть батьківську область, до якої прив’язана кожна вільна змінна:
    1. consoleбуде пов'язаний з глобальною області.
  3. У якій області посилається функція ? Глобальний масштаб .
    1. Тому consoleНЕ зімкнулися на f.

Таким чином, функція fне є закриттям.

Для g:

  1. Перерахуйте змінні:
    1. consoleє вільною змінною.
    2. i2є вільною змінною.
  2. Знайдіть батьківську область, до якої прив’язана кожна вільна змінна:
    1. consoleбуде пов'язаний з глобальною області.
    2. i2буде пов'язаний з областю f.
  3. У якій області посилається функція ? ОбсягsetTimeout .
    1. Тому consoleНЕ зімкнулися на g.
    2. Отже , i2буде закрита в протягом від g.

Таким чином, функція gє закриттям для вільної змінної i2(що є надмірним значенням g), коли на неї посилається зсередини setTimeout.

Добре для вас: Ви використовуєте закриття. Внутрішня функція - це закриття.

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

Редагування: просте пояснення, чому закриваються всі функції (кредити @ Петер):

Спочатку розглянемо наступну програму (це контроль ):

lexicalScope();

function lexicalScope() {
    var message = "This is the control. You should be able to see this message being alerted.";

    regularFunction();

    function regularFunction() {
        alert(eval("message"));
    }
}

  1. Ми знаємо, що і вище, lexicalScopeі regularFunctionне є закриттями з наведеного визначення .
  2. Коли ми виконуємо програму, ми очікуємо, message що вона буде оповіщена, оскільки regularFunction вона не є закритою (тобто вона має доступ до всіх змінних у своїй батьківській області - включаючи message).
  3. Коли ми виконуємо програму, ми спостерігаємо, що messageвона дійсно оповіщена.

Далі розглянемо наступну програму (це альтернатива ):

var closureFunction = lexicalScope();

closureFunction();

function lexicalScope() {
    var message = "This is the alternative. If you see this message being alerted then in means that every function in JavaScript is a closure.";

    return function closureFunction() {
        alert(eval("message"));
    };
}

  1. Ми знаємо, що лише наведене вище визначенняclosureFunction є закриттям .
  2. Коли ми виконуємо програму, ми очікуємо, що вона message не буде оповіщена, оскільки closureFunction це закриття (тобто вона має лише доступ до всіх її нелокальних змінних під час створення функції ( див. Цю відповідь ) - це не включає message).
  3. Коли ми виконуємо програму, ми спостерігаємо, що messageнасправді оповіщено.

Що ми робимо з цього?

  1. Інтерпретатори JavaScript не ставляться до закриттів по-різному від способів, що стосуються інших функцій.
  2. Кожна функція несе разом із нею свою сферу застосування . Закриття не мають окремого середовища реферування.
  3. Закриття - це як і будь-яка інша функція. Ми просто називаємо їх закриттями, коли вони посилаються на сферу, що виходить за рамки, до яких вони належать, оскільки це цікавий випадок.

40
Прийнято, тому що ви дуже детально описуєтесь, пояснюючи дуже приємно, що відбувається. І нарешті, я тепер зрозумів краще, що таке закриття, або краще сказати: як працює змінна прив'язка в JS.
leemes

3
У випадку 1 ви говорите, що це gпрацює в межах setTimeout, але у випадку 2 ви говорите, що це fпрацює в глобальному масштабі. Вони обидва в межах setTimeout, і в чому різниця?
rosscj2533

9
Будь ласка, повідомте про це свої джерела? Я ніколи не бачив визначення, де функція може бути закриттям, якщо вона викликається в одній області, а не в іншій. Таким чином, це визначення виглядає як підмножина більш загального визначення, до якого я звик (див . Відповідь КЕВ ), де закриття є закриттям, це закриття незалежно від сфери його виклику , або навіть якщо воно ніколи не називається!
Briguy37

11
@AaditMShah Я згоден з вами щодо того, що таке закриття, але ви говорите так, ніби є різниця між звичайними функціями та закриттями в JavaScript. Різниці немає; всередині кожної функції буде посилатися на конкретний ланцюг сфери, в якій вона була створена. Двигун JS не вважає це іншим випадком. Немає необхідності у складному контрольному списку; просто знайте, що кожен об'єкт функції несе лексичну область. Той факт, що змінні / властивості є глобально доступними, не робить функцію менш закритою (це просто марний випадок).
Пітер

13
@Peter - Ви знаєте що, ви маєте рацію. Немає різниці між звичайною функцією і закриттям. Я провів тест, щоб довести це, і це призводить на вашу користь: ось контроль і ось альтернатива . Те, що ви говорите, має сенс. Інтерпретатору JavaScript потрібно спеціально вести бухгалтерію для закриття. Вони є просто побічними продуктами лексично обмеженої мови з функціями першого класу. Мої знання обмежувалися тим, що я читав (що було помилковим). Дякую, що виправили мене. Я оновлю свою відповідь, щоб відобразити те саме.
Аадіт М Шах

96

Відповідно до closureвизначення:

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

Ви використовуєте, closureякщо ви визначаєте функцію, яка використовує змінну, яка визначена поза функцією. (ми називаємо змінну вільною змінною ).
Всі вони використовують closure(навіть у 1-му прикладі).


1
Як третя версія використовує змінну, визначену поза функцією?
Джон

1
@Jon повертається використання функції, i2яке визначено зовні.
кев

1
@kev Ви використовуєте закриття, якщо ви визначаєте функцію, яка використовує змінну, яка визначена поза функцією ...... тоді у "Випадок 1: Програма вашого друга" "Aadit M Shah" відповідь - "функція f" закриття? він використовує i (змінна, яка визначена поза функцією). чи посилається глобальна область на детермінант?
внутрішні-в


54

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

Давайте подивимось більш детальне пояснення. Для розуміння закриттів важливо зрозуміти, як JavaScript застосовує змінні.

Області застосування

У JavaScript області застосування визначаються функціями. Кожна функція визначає нову область.

Розглянемо наступний приклад;

function f()
{//begin of scope f
  var foo='hello'; //foo is declared in scope f
  for(var i=0;i<2;i++){//i is declared in scope f
     //the for loop is not a function, therefore we are still in scope f
     var bar = 'Am I accessible?';//bar is declared in scope f
     console.log(foo);
  }
  console.log(i);
  console.log(bar);
}//end of scope f

заклик f відбитків

hello
hello
2
Am I Accessible?

Розглянемо тепер випадок, коли у нас є функція, gвизначена в іншій функції f.

function f()
{//begin of scope f
  function g()
  {//being of scope g
    /*...*/
  }//end of scope g
  /*...*/
}//end of scope f

Ми будемо називати fв лексичний батько з g. Як було пояснено раніше, ми маємо 2 сфери; сфера fта сфера застосування g.

Але одна сфера дії знаходиться «в межах» іншої сфери, тож чи є сфера дочірньої функції частиною сфери батьківської функції? Що відбувається зі змінними, задекларованими в області батьківської функції; чи зможу я отримати доступ до них із сфери функціонування дитини? Саме там вступають закриття.

Закриття

У JavaScript функція gможе не тільки отримувати доступ до будь-яких змінних, оголошених в області дії, gале також отримати доступ до будь-яких змінних, оголошених у межах батьківської функції f.

Розглянемо наступне;

function f()//lexical parent function
{//begin of scope f
  var foo='hello'; //foo declared in scope f
  function g()
  {//being of scope g
    var bar='bla'; //bar declared in scope g
    console.log(foo);
  }//end of scope g
  g();
  console.log(bar);
}//end of scope f

заклик f відбитків

hello
undefined

Давайте розглянемо рядок console.log(foo);. На даний момент ми знаходимося в області дії, gі ми намагаємося отримати доступ до змінної, fooяка оголошена в області застосування f. Але, як зазначено раніше, ми можемо отримати доступ до будь-якої змінної, оголошеної в лексичній батьківській функції, що в цьому випадку; gє лексичним батьком f. Тому helloдрукується.
Давайте тепер розглянемо рядок console.log(bar);. На даний момент ми знаходимося в області дії, fі ми намагаємося отримати доступ до змінної, barяка оголошена в області застосування g. barне задекларовано в поточному обсязі і функція gне є батьківською f, тому barне визначена

Насправді ми також можемо отримати доступ до змінних, задекларованих у межах лексичної функції "великий батько". Тому, якщо в функції буде hвизначена функціяg

function f()
{//begin of scope f
  function g()
  {//being of scope g
    function h()
    {//being of scope h
      /*...*/
    }//end of scope h
    /*...*/
  }//end of scope g
  /*...*/
}//end of scope f

то hзможе отримати доступ до всіх змінним , оголошеним в області видимості функції h, gі f. Це робиться із закриттями . Закриття JavaScript дозволяє нам отримувати доступ до будь-якої змінної, оголошеної у лексичній батьківській функції, у лексичній грандіозній батьківській функції, у лексичній функції grand-grand parent тощо. Це може розглядатися як ланцюг діапазону ; scope of current function -> scope of lexical parent function -> scope of lexical grand parent function -> ... до останньої батьківської функції, яка не має лексичного батьківського.

Об'єкт вікна

Насправді ланцюг не зупиняється на останній батьківській функції. Є ще одна спеціальна сфера; глобальний масштаб . Кожна змінна, не оголошена у функції, вважається оголошеною у глобальній області. Світова сфера має дві спеціальності;

  • кожна змінна, оголошена в глобальному масштабі, доступна всюди
  • змінні, оголошені в глобальній області, відповідають властивостям windowоб'єкта.

Тому існують рівно два способи оголошення змінної fooу глобальному масштабі; або не оголосивши його у функції, або встановивши властивість fooоб’єкта вікна.

Обидві спроби використовують закриття

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

Давайте створимо нову мову програмування; JavaScript-без закриття. Як випливає з назви, JavaScript-No-Closure є ідентичним JavaScript, за винятком того, що він не підтримує закриття.

Іншими словами;

var foo = 'hello';
function f(){console.log(foo)};
f();
//JavaScript-No-Closure prints undefined
//JavaSript prints hello

Добре, давайте подивимося, що відбувається з першим рішенням із JavaScript-No-Closure;

for(var i = 0; i < 10; i++) {
  (function(){
    var i2 = i;
    setTimeout(function(){
        console.log(i2); //i2 is undefined in JavaScript-No-Closure 
    }, 1000)
  })();
}

тому це буде надруковано undefined10 разів у JavaScript без закриття.

Отже, перше рішення використовує закриття.

Давайте розглянемо друге рішення;

for(var i = 0; i < 10; i++) {
  setTimeout((function(i2){
    return function() {
        console.log(i2); //i2 is undefined in JavaScript-No-Closure
    }
  })(i), 1000);
}

тому це буде надруковано undefined10 разів у JavaScript без закриття.

Обидва рішення використовують закриття.

Редагувати: Передбачається, що ці 3 фрагменти коду не визначені в глобальній області. В іншому випадку змінні fooта iбудуть прив’язані до windowоб'єкта і, отже, доступні через windowоб'єкт як у JavaScript, так і в JavaScript-не-закритті.


Чому слід не iвизначатись? Ви просто посилаєтесь на батьківську область, яка все ще діє, якщо не було закриттів.
leemes

з тієї ж причини, що foo не визначено в JavaScript-No-Closure. <code> i </code> не визначений у JavaScript завдяки функції в JavaScript, яка дозволяє отримувати доступ до змінних, визначених у лексичному батьківщині. Ця особливість називається закриттям.
брилеут

Ви не зрозуміли різниці між посиланням на вже визначені змінні та вільними змінними. При закритті ми визначаємо вільні змінні, які повинні бути пов'язані у зовнішньому контексті. У коді, ви просто встановити i2 в iтой час , коли ви визначаєте функцію. Це робить iНЕ безкоштовну змінну. Все ж ми вважаємо вашу функцію закриттям, але без жодної вільної змінної, в цьому справа.
leemes

2
@leemes, я згоден І порівняно з прийнятою відповіддю, це насправді не показує, що насправді відбувається. :)
Abel

3
Я думаю, що це найкраща відповідь, пояснюючи закриття взагалі та просто, а потім перейшов до конкретного випадку використання. Дякую!
tim peterson

22

Я ніколи не був задоволений тим, як це хтось пояснює.

Ключ до розуміння закриття - зрозуміти, як виглядатиме JS без закриття.

Без закриття це призведе до помилки

function outerFunc(){
    var outerVar = 'an outerFunc var';
    return function(){
        alert(outerVar);
    }
}

outerFunc()(); //returns inner function and fires it

Після того, як externalFunc повернувся у уявній версії JavaScript, відключеній, посилання на externalVar буде зібрано сміття і не залишиться нічого, щоб внутрішній функціонал посилався.

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

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

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

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

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

function outerFunc(){
    var incrementMe = 0;
    return function(){ incrementMe++; console.log(incrementMe); }
}
var inc = outerFunc();
inc(); //logs 1
inc(); //logs 2

Ви маєте рацію щодо "знімка" (я думаю, ви посилаєтесь на мою відповідь) цим. Я шукав слово, яке вказувало б на поведінку. У вашому прикладі це може розглядатися як конструкція закриття «гарячої лінії». Під час фіксації закриття як параметра у внутрішній функції, можна констатувати, що він поводиться як "знімок". Але я згоден, неправильно використані слова лише додають плутанини темі. Якщо у вас є якісь пропозиції щодо цього, я оновлю свою відповідь.
Андріс

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

Без закриття ви отримаєте помилку, оскільки ви намагаєтесь використовувати змінну, яка не існує.
Хуан Мендес

Хм ... хороший момент. Чи посилання на невизначений var ніколи не приводив до помилки, оскільки в кінцевому підсумку це буде виглядати як властивість глобального об'єкта, або я плутаю його з присвоєнням невизначеним параметрам?
Ерік Реппен

17

Ви обидва використовуєте закриття.

Я збираюся з визначенням Вікіпедії тут:

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

Спроба вашого друга чітко використовує змінну i, яка не є локальною, приймаючи її значення та створюючи копію для зберігання в локальній i2.

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


Так, але я думаю, що справа в тому, як він це робить. Він просто копіює iв i2, а потім визначає деяку логіку і виконує цю функцію. Якби я не виконав його відразу, а зберігав у варі та виконував після циклу, він надрукував би 10, чи не так? Так воно не захопило i.
leemes

6
@leemes: Це було iчудово. Поведінка, яку ви описуєте, не є результатом закриття та не закриття; це результат зміни змінної закритого типу. Ви робите те саме, використовуючи різні синтаксиси, негайно викликаючи функцію та передаючи iяк аргумент (який копіює її поточне значення на місці). Якщо ви помістите своє setTimeoutвсередину іншого setTimeout, відбудеться те ж саме.
Джон

13

Ви та ваш друг використовуєте закриття:

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

MDN: https://developer.mozilla.org/en-US/docs/JavaScript/Guide/Closures

У коді функція вашого друга function(){ console.log(i2); }визначається всередині закриття анонімної функції function(){ var i2 = i; ...і може читати / записувати локальну змінну i2.

У вашому коді функція function(){ console.log(i2); }визначена всередині закриття функції function(i2){ return ...і може читати / записувати місцеві цінні i2(оголошені в цьому випадку як параметри).

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

Інший еквівалент (але з меншим використанням пам'яті):

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

1
Я не розумію, чому ваше рішення проти рішення мого друга "швидше і з меншим використанням пам'яті", ви могли б детальніше розглянути?
brillout

3
У своєму рішенні ви створюєте 20 функціональних об'єктів (по 2 об’єкти на кожному циклі: 2x10 = 20). Той самий результат у вирішенні вашого френда. У рішенні "мій" створюється лише 11 функціональних об'єктів: 1 перед циклом і 10 "всередині" - 1 + 1x10 = 11. Як результат - менше використання пам'яті та збільшення швидкості.
Ендрю Д.

1
Теоретично це було б правдою. На практиці також: Дивіться цей орієнтир JSPerf
Rob W

10

Закриття

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

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

Ще раз (naïf): закриття має доступ до змінних, які не передаються як параметр.

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

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

Отже, щодо ваших прикладів:

// 1
for(var i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i); // closure, only when loop finishes within 1000 ms,
    }, 1000);           // i = 10 for all functions
}
// 2
for(var i = 0; i < 10; i++) {
    (function(){
        var i2 = i; // closure of i (lexical scope: for-loop)
        setTimeout(function(){
            console.log(i2); // closure of i2 (lexical scope:outer function)
        }, 1000)
    })();
}
// 3
for(var i = 0; i < 10; i++) {
    setTimeout((function(i2){
        return function() {
            console.log(i2); // closure of i2 (outer scope)

        }
    })(i), 1000); // param access i (no closure)
}

Усі використовують закриття. Не плутайте точку виконання з закриттям. Якщо "знімок" закриття зроблений в неправильний момент, значення можуть бути несподіваними, але, безумовно, зроблено закриття!



10

Давайте розглянемо обидва способи:

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

Декларує та негайно виконує анонімну функцію, яка працює setTimeout()у власному контексті. Поточне значення iзберігається, роблячи копію i2першою; він працює через негайне виконання.

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

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

Важливо

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

Якщо вставити обидва коди всередину іншого setTimeout(), не доводить, що лише другий підхід використовує закриття, для початку просто не те саме.

Висновок

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


Я думаю, що різниця полягає в тому, що його рішення (1-й) фіксує посиланням, моє (2-е) - фіксує за значенням. У цьому випадку це не має ніякого значення, але якби я ставлю виконання в інший setTimeout, ми побачимо, що у його рішення є проблема, що він використовує кінцеве значення i, а не поточне, тоді як мій підвіконня використовує поточне значення (оскільки захоплене значенням).
leemes

@leemes Ви обоє захоплюєте однаково; передача змінної через аргумент функції або призначення - це одне і те ж ... ви могли б додати до свого питання, як би ви перетворили виконання в інше setTimeout()?
Як

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

Зачекайте, ви не передали функцію (зовнішній) setTimeout. Видаліть їх (), таким чином передаючи функцію, і ви бачите в 10 разів більше виходу 10.
leemes

@leemes Як згадувалося раніше, ()саме його змушує працювати його код, як і ваш (i); ви не просто загорнули його код, ви внесли зміни до нього .. тому ви вже не можете зробити дійсне порівняння.
Ja͢ck

8

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

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

var z = 1;
function x(){
    var z = 2; 
    y(function(){
      alert(z);
    });
}
function y(f){
    var z = 3;
    f();
}
x(); //alerts '2' 

6

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

У вашому випадку з друзями iдоступ до анонімної функції 1 та i2доступ до анонімної функції 2, де console.logвона присутня.

У вашому випадку ви отримуєте доступ до i2анонімної функції, де console.logвона присутня. Додайте debugger;оператор до console.logта в інструменти для розробників Chrome в розділі "Змінні області", воно покаже, під якою сферою знаходиться змінна.


2
Розділ "Закриття" на правій панелі використовується, оскільки немає конкретнішої назви. "Локальний" є більш сильною ознакою, ніж "Закриття".
Роб Ш


4

Розглянемо наступне. Це створює та відтворює функцію, fяка закривається i, але різні !:

i=100;

f=function(i){return function(){return ++i}}(0);
alert([f,f(),f(),f(),f(),f(),f(),f(),f(),f(),f()].join('\n\n'));

f=function(i){return new Function('return ++i')}(0);        /*  function declarations ~= expressions! */
alert([f,f(),f(),f(),f(),f(),f(),f(),f(),f(),f()].join('\n\n'));

а наступне закривається на "a" функцію "себе"
(самі! фрагмент після цього використовує єдиний референт f)

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

або щоб бути більш чітким:

for(var i = 0; i < 10; i++) {
    console.log(    f = new Function( 'console.log('+i+')' )    );
    setTimeout( f,  1000 );
}

NB. Останнім визначення fIS , function(){ console.log(9) } перш ніж 0 буде надруковано.

Кава! Концепція закриття може бути примусовим відволіканням від сутності елементарного програмування:

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

x-ref .:
Як працюють закриття JavaScript?
Пояснення закриттів Javascript
Потрібно, якщо закриття (JS) вимагає функції всередині функції
Як зрозуміти закриття в Javascript?
Плутанина локальної та глобальної змінної Javascript


фрагменти спробували вперше - не знаю, як контролювати - Run' only was desired - not sure how to remove the Copy`
ekim

-1

Я хотів би поділитися своїм прикладом та поясненням щодо закриття. Я зробив приклад пітона та дві фігури для демонстрації станів стека.

def maker(a, b, n):
    margin_top = 2
    padding = 4
    def message(msg):
        print('\n * margin_top, a * n, 
            ' ‘ * padding, msg, '  * padding, b * n)
    return message

f = maker('*', '#', 5)
g = maker('', '♥’, 3)

f('hello')
g(‘good bye!')

Вихід цього коду буде таким:

*****      hello      #####

      good bye!    ♥♥♥

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

коли функція повертається від виробника

коли функція викликається пізніше

Коли функція викликається через параметр або нелокальну змінну, коду потрібні локальні прив'язки змінної, такі як margin_top, padding, а також a, b, n. Для того, щоб забезпечити функціонування коду функції, кадр стека функцій виробника, який уже давно відійшов, повинен бути доступним, що створюється резервною копією у закритій частині, яку ми можемо знайти разом із об'єктом повідомлення функції.


Я хотів би зняти цю відповідь. Я зрозумів, що питання не в тому, що є закриттям, тому я хотів би перенести його на інше питання.
Eunjung Lee

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