Подтвердить что ты не робот

Закрытие Javascript - вопрос с переменной зоной

Я читаю сайт разработчика 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" прямо над циклом for? Затем он имеет область setupHelp(), и каждая итерация присваивает ему другое значение, которое затем будет записано в качестве текущего значения в закрытии... правильно?

4b9b3361

Ответ 1

Его, потому что в момент времени 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 не имеет области блока, но имеет функцию-scope. Создав закрытие, мы постоянно фиксируем ссылку на helpText[i].

Ответ 2

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

Это помогает понять, как 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 и т.д. Если это С#, с другой стороны, для каждого цикла будет создан новый объект области видимости, и каждое закрытие будет содержать указатель на его собственный уникальный объем.
  • Обратите внимание, что если anonymousFunction1 изменяет переменную в своей области действия, она изменяет эту переменную для других анонимных функций. Это может привести к некоторым действительно странным взаимодействиям.
  • Области - это просто объекты, такие как те, которые вы программируете. В частности, это словари. Виртуальная машина JS управляет их удалением из памяти, как и все остальное, - с сборщиком мусора. По этой причине чрезмерное использование закрытий может создать настоящую память. Поскольку замыкание содержит указатель на объект области видимости (который, в свою очередь, содержит указатель на объект родительской области видимости и вкл. И далее), вся цепочка цепей не может быть собрана в мусор и должна храниться в памяти.

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

Ответ 3

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

// 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 array.prototype.forEach, который исправляет проблему с областью.

helpText.forEach(function(help){
   document.getElementById(help.id).onfocus = function(){
      showHelp(help);
   };
});

Ответ 4

Новые области создаются только в блоках functionwith, но не используют их). Циклы, подобные for, не создают новые области.

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

Ответ 5

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