Javascript замыкания - вопрос переменной области - PullRequest
23 голосов
/ 18 июля 2010

Я читаю сайт разработчика Mozilla по замыканиям и заметил в их примере типичные ошибки, у них был следующий код:

<p id="help">Helpful notes will appear here</p>  
<p>E-mail: <input type="text" id="email" name="email"></p>  
<p>Name: <input type="text" id="name" name="name"></p>  
<p>Age: <input type="text" id="age" name="age"></p>  

и

function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

и они сказали, что для события onFocus в коде будет показана справка только для последнего элемента, потому что все анонимные функции, назначенные событию onFocus, имеют замыкание вокруг переменной 'item', что имеет смысл, поскольку в переменных JavaScript не имеют объема блока. Решение состояло в том, чтобы использовать вместо этого 'let item = ...', поскольку тогда он имеет область видимости блока.

Однако меня интересует, почему вы не можете объявить 'var item' прямо над циклом for? Затем он имеет область действия setupHelp (), и на каждой итерации вы присваиваете ему другое значение, которое затем будет записываться как его текущее значение в замыкании ... верно?

Ответы [ 5 ]

26 голосов
/ 18 июля 2010

Это потому, что во время оценки item.help цикл завершился бы полностью. Вместо этого вы можете сделать это с закрытием:

for (var i = 0; i < helpText.length; i++) {
   document.getElementById(helpText[i].id).onfocus = function(item) {
           return function() {showHelp(item.help);};
         }(helpText[i]);
}

JavaScript не имеет области видимости блока, но имеет область действия. Создавая замыкание, мы постоянно фиксируем ссылку на helpText[i].

20 голосов
/ 18 июля 2010

Закрытие - это функция и область действия этой функции.

Это помогает понять, как Javascript реализует область действия в этом случае. На самом деле это всего лишь серия вложенных словарей. Рассмотрим этот код:

var global1 = "foo";

function myFunc() {
    var x = 0;
    global1 = "bar";
}

myFunc();

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

{ global1: "foo", myFunc:<function code> }

Скажем, вы вызываете myFunc, у которого есть локальная переменная x. Новая область создается для выполнения этой функции. Локальная область действия функции выглядит следующим образом:

{ x: 0 }

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

{ x: 0, parentScope: { global1: "foo", myFunc:<function code> } }

Это позволяет myFunc изменять global1. В Javascript, когда вы пытаетесь присвоить значение переменной, он сначала проверяет локальную область для имени переменной. Если он не найден, он проверяет parentScope, parentScope этой области и т. Д., Пока не будет найдена переменная.

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

setupHelpScope = {
  helpText:<...>,
  i: 3, 
  item: {'id': 'age', 'help': 'Your age (you must be over 16)'},
  parentScope: <...>
}

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

[anonymousFunction1, setupHelpScope]
[anonymousFunction2, setupHelpScope]
[anonymousFunction3, setupHelpScope]

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

Чтобы ответить на ваш вопрос, не имеет значения, добавляете ли вы var item над циклом for или внутри него. Поскольку циклы for не создают свою собственную область, item будет храниться в словаре областей текущей функции, который равен setupHelpScope. Корпуса, созданные внутри цикла for, всегда будут указывать на setupHelpScope.

Некоторые важные заметки:

  • Это происходит потому, что в Javascript циклы for не имеют собственной области видимости - они просто используют область действия включающей функции. Это также верно для if, while, switch и т. Д. Если бы это был C #, с другой стороны, для каждого цикла был бы создан новый объект области, и каждое замыкание содержало бы указатель на свой собственный уникальный объем.
  • Обратите внимание, что если anonymousFunction1 изменяет переменную в своей области, она изменяет эту переменную для других анонимных функций. Это может привести к некоторым действительно странным взаимодействиям.
  • Области действия - это просто объекты, подобные тем, которые вы программируете. В частности, это словари. Виртуальная машина JS управляет их удалением из памяти, как и все остальное - с помощью сборщика мусора. По этой причине чрезмерное использование замыканий может привести к увеличению объема памяти. Поскольку замыкание содержит указатель на объект области действия (который, в свою очередь, содержит указатель на его родительский объект области действия и так далее и далее), вся цепочка области действия не может быть собрана мусором и должна оставаться в памяти.

Дальнейшее чтение:

3 голосов
/ 15 мая 2015

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

// Function only exists once in memory
function doOnFocus() {
   // ...but you make the assumption that it'll be called with
   //    the right "this" (context)
   var item = helpText[this.index];
   showHelp(item.help);
};

for (var i = 0; i < helpText.length; i++) {
   // Create the special context that the callback function
   // will be called with. This context will have an attr "i"
   // whose value is the current value of "i" in this loop in
   // each iteration
   var context = {index: i};

   document.getElementById(helpText[i].id).onfocus = doOnFocus.bind(context);
}

Если вы хотите однострочник (или близко к нему):

// Kind of messy...
for (var i = 0; i < helpText.length; i++) {
   document.getElementById(helpText[i].id).onfocus = function(){
      showHelp(helpText[this.index].help);
   }.bind({index: i});
}

Или, что еще лучше, вы можете использовать EcmaScript 5.1's array.prototype.forEach, который решает проблему с областью видимости.

helpText.forEach(function(help){
   document.getElementById(help.id).onfocus = function(){
      showHelp(help);
   };
});
2 голосов
/ 18 июля 2010

Новые области только , созданные в function блоках (и with, но не используйте их). Такие циклы, как for, не создают новых областей действия.

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

1 голос
/ 18 июля 2010

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...