Javascript: создание функций в цикле

Недавно мне показалось, что мне нужно создать массив функций. Функции используют значения из XML-документа, и я запускаю соответствующие узлы с циклом for. Однако после этого я обнаружил, что только все последние узлы листа XML (соответствующие последнему прогону цикла for) всегда использовались всеми функциями в массиве.

Ниже приведен пример, демонстрирующий это:

var numArr = [];
var funArr = [];
for(var i = 0; i < 10; ++i){
    numArr[numArr.length] = i;
    funArr[funArr.length] = function(){  return i; };
}

window.alert("Num: " + numArr[5] + "\nFun: " + funArr[5]());

Выход: Num: 5 и Fun: 10.

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

var funArr2 = [];
for(var i = 0; i < 10; ++i)
    funArr2[funArr2.length] = (function(i){ return function(){ return i;}})(i);

window.alert("Fun 2: " + funArr2[5]());

Я знаю, что это связано с определением области обзора, но на первый взгляд кажется, что он не будет отличаться от моего наивного подхода. Я как-то новичок в Javascript, поэтому, если я могу спросить, почему использование этой функции-возвращаемой функции исключает проблему обзора? Кроме того, почему (i) включен в конце?

Заранее большое спасибо.

Ответ 1

Второй метод немного ясен, если вы используете имя параметра, которое не маскирует имя переменной цикла:

funArr[funArr.length] = (function(val) { return function(){  return val; }})(i);

Проблема с вашим текущим кодом заключается в том, что каждая функция является закрытием, и все они ссылаются на одну i ту же переменную i. Когда каждая функция запускается, она возвращает значение i во время запуска функции (что будет больше, чем предельное значение для цикла).

Более четким способом было бы написать отдельную функцию, которая возвращает закрытие, которое вы хотите:

var numArr = [];
var funArr = [];
for(var i = 0; i < 10; ++i){
    numArr[numArr.length] = i;
    funArr[funArr.length] = getFun(i);
}

function getFun(val) {
    return function() { return val; };
}

Обратите внимание, что в моем ответе это в основном то же самое, что и первая строка кода: вызов функции, возвращающей функцию и передающей значение i в качестве параметра. Главное преимущество - ясность.

EDIT: теперь, когда EcmaScript 6 поддерживается почти повсеместно (извините, пользователи IE), вы можете обойтись с помощью более простого подхода - используйте ключевое слово let вместо var для переменной цикла:

var numArr = [];
var funArr = [];
for(let i = 0; i < 10; ++i){
    numArr[numArr.length] = i;
    funArr[funArr.length] = function(){  return i; };
}

С этим небольшим изменением каждый элемент funArr является замыканием, связанным с другим объектом i на каждой итерации цикла. Для получения дополнительной информации о let, см. Этот пост Mozilla Hacks с 2015 года. (Если вы настроите таргетинг на среды, которые не поддерживают let, придерживайтесь того, что я написал ранее, или запустите это через transpiler перед использованием.

Ответ 2

Давайте рассмотрим, что делает код немного ближе и присваивает имена мнимой функции:

(function outer(i) { 
    return function inner() { 
        return i;
    }
 })(i);

Здесь outer принимает аргумент i. JavaScript использует функцию scoping, что означает, что каждая переменная существует только внутри функции, в которой она определена. i здесь определяется outer, и поэтому существует во outer (и любых областях, заключенных внутри).

inner содержит ссылку на переменную i. (Обратите внимание, что он не переопределяет i в качестве параметра или ключевое слово var !) Правила определения области видимости JavaScript указывают, что такая ссылка должна быть привязана к первой охватывающей области, которая здесь является областью outer. Поэтому i внутри inner относится к тому же i что и внутри outer.

Наконец, после определения функции outer, мы сразу вызываем ее, передавая ей значение i (которое представляет собой отдельную переменную, определенную в самой внешней области). Значение i заключено в outer, и его значение теперь не может быть изменено никаким кодом в самой внешней области. Таким образом, когда внешний i увеличивается в цикле for, i внутри outer сохраняет одно и то же значение.

Помня, что мы на самом деле создали ряд анонимных функций, каждый со своими собственными значениями и значениями аргументов, мы надеемся, ясно, как каждая из этих анонимных функций сохраняет свое значение для i.

Наконец, для полноты рассмотрим, что произошло с исходным кодом:

for(var i = 0; i < 10; ++i){
    numArr[numArr.length] = i;
    funArr[funArr.length] = function(){  return i; };
}

Здесь мы видим, что анонимная функция содержит ссылку на внешний i. По мере изменения этого значения он будет отражен в анонимной функции, которая не сохраняет свою собственную копию значения в любой форме. Таким образом, поскольку i == 10 во внешней области в то время, когда мы идем, и вызываем все эти функции, которые мы создали, каждая функция вернет значение 10.

Ответ 3

Я рекомендую собрать книгу, подобную JavaScript: The Definitive Guide, чтобы получить более глубокое понимание JavaScript в целом, чтобы вы могли избежать подобных ошибок.

Этот ответ также дает достойное объяснение о закрытии:

Как работают блокировки JavaScript?

Когда вы вызываете

function() { return i; }

функция фактически выполняет переменный поиск родительского объекта-объекта (области), в котором определяется i. В этом случае я определяется как 10, и поэтому каждая из этих функций вернется 10. Причина, по которой это работает

(function(i){ return function(){ return i;}})(i);

заключается в том, что при непосредственном вызове анонимной функции создается новый объект-вызов, в котором определяется текущий i. Поэтому, когда вы вызываете вложенную функцию, эта функция ссылается на объект-вызов анонимной функции (которая определяет любое значение, которое вы передавали ему при ее вызове), а не область, в которой я был первоначально определен (который все еще равен 10),