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),