Можно ли объяснить это странное поведение с let и var?

Следующий пример кода меня смущает...

"use strict";

var filesToLoad = [ 'fileA','fileB','fileC' ];
var promiseArray = [];

for( let i in filesToLoad ) {
  promiseArray.push(
    new Promise( function(resolve, reject ) {
      setTimeout( function() {
        resolve( filesToLoad[i] );
      }, Math.random() * 1000 );
    })
  );
}

Promise.all( promiseArray ).then(function(value) {
  console.log(value);
});

Причина, по которой я смущен, заключается в том, что я ожидал случайного упорядоченного вывода на консоль. Но я всегда получаю следующее...

['файлA', 'файлB', 'файлC']

Меня это, мягко говоря, немного смущает, но что действительно заставляет меня почесать голову, так это то, что когда я меняю let i на var i, я получаю следующий результат....

['файлC', 'файлC', 'файлC']

Как человек, который только недавно пытался полностью понять Promises и не так давно начал использовать let, я действительно в тупике.

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

Получив много отличных ответов, я переработал пример, чтобы избавиться от цикла и i. Большинству покажется очевидным, но для меня забавным...

"use strict";

var filesToLoad = [ 'fileA','fileB','fileC' ];

function start( name )  {
    return new Promise( function(resolve, reject ) {
      setTimeout( function() {
        resolve( name + '_done' );
      }, Math.random() * 1000 );
    });
}

Promise.all( filesToLoad.map(start) ).then(function(value) {
  console.log(value);
});

person thomas-peter    schedule 03.12.2015    source источник
comment
Спасибо всем, кто ответил. Это действительно помогает понять.   -  person thomas-peter    schedule 03.12.2015
comment
Это очень популярный вопрос о SO: закрытие JavaScript внутри циклов - простой практический пример   -  person Felix Kling    schedule 03.12.2015


Ответы (4)


Это из-за закрытия. Прочтите об этом here и here.

Также let относится к блоку, тогда как var относится к функции.

В случае использования var i:

После тайм-аута, когда функция запускается, цикл был завершен, и я был установлен на 2, поэтому он был разрешен с помощью filesToLoad[2] для всех функций setTimeout.

В случае использования let i:

Поскольку это блочная область, когда функция разрешается, она запоминает состояние i, когда было объявлено setTimeOut, поэтому, когда она разрешается, она использует правильное значение i.

Относительно порядка вывода в случае использования let i.

Promise.all(Iterable<any>|Promise<Iterable<any>> input) -> Promise

Учитывая Iterable (массивы являются Iterable) или обещание Iterable, которое производит обещания (или сочетание обещаний и значений), перебирает все значения в Iterable в массив и возвращает обещание, которое выполняется, когда все элементы в массиве выполнены. Значение выполнения обещания представляет собой массив со значениями выполнения в соответствующих позициях исходного массива. Если какое-либо обещание в массиве отклоняется, возвращенное обещание отклоняется с указанием причины отклонения.

Таким образом, независимо от порядка, в котором ваши обещания разрешаются, результат promise.all всегда будет иметь значение разрешения обещания в правильном порядке.

person Kunal Kapadia    schedule 03.12.2015

Почему использование let и var приводит к разным результатам:

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

Определение переменной var в заголовке цикла for не означает, что она существует только на протяжении всего цикла for, как вы заметите, если сделаете следующее:

for (var i = 0; i < 10; i++) { /*...*/ }

console.log(i); //=> 10

// `i` is already declared and its value will be 10
for (; i < 20; i++) { /*...*/ }

console.log(i); //=> 20

Вы можете вообще избежать этой проблемы, если будете использовать Array#forEach< /a>, который выполняет работу filesToLoad[i] за вас, предоставляя вам следующее значение в функции обратного вызова на каждой итерации:

filesToLoad.forEach((file) => {
  promiseArray.push(
    new Promise( function(resolve, reject ) {
      setTimeout( function() {
        resolve( file );
      }, Math.random() * 1000 );
    })
  );
});

______

Влияет ли использование let или var на поведение Promise#all?

Нет. В вашем примере положение обещаний в promiseArray определяет, в каком порядке значения добавляются в массив результатов, а не когда разрешается каждое из этих обещаний. Тот факт, что вы разрешаете промисы через случайные промежутки времени, не меняет позицию разрешенного значения в пределах promiseArray. Вы продемонстрировали, что Promise#all создает массив значений, позиции которых сопоставляются с обещанием, создавшим их значение.

См. этот ответ для получения дополнительной информации о поведении Promise#all:

Все это означает, что выходные данные строго упорядочены как входные данные, если входные данные строго упорядочены (например, массив).

person sdgluck    schedule 03.12.2015
comment
Ваш пример может быть еще более лаконичным с Array.prototype.map: const promiseArray = filesToLoad.map(file => new Promise(/**/)); - person Paolo Moretti; 03.12.2015
comment
@PaoloMoretti Спасибо :) Я предпочел знакомство с исходным примером, а не введение новых концепций ради понимания OP. (Но вы абсолютно правы!) - person sdgluck; 03.12.2015

Это потому, что область видимости var не является дочерней по отношению к for, а родственной ей. Итак, цикл выполняется один раз и устанавливает i = 0. Затем он запускается еще 2 раза и устанавливает i = 1, а затем i = 2. После того, как все это произошло, запускаются тайм-ауты, и все запускают функцию разрешения и проходят в resolve( filesToLoad[2] ). let работает правильно, потому что значение let i не переопределяется следующими итерациями цикла.

Короче говоря, тайм-аут запускается только после того, как цикл уже запущен 3 раза и, следовательно, проходит одно и то же значение. Я создал jsfiddle рабочей версии, используя var.

person Jamy    schedule 03.12.2015

На самом деле это две разные, не связанные друг с другом проблемы.

Ваше первое наблюдение, что вывод находится в исходном порядке, а не в случайном порядке, связано с поведением Promise.all. Promise.all возвращает новый промис, который разрешается в массив, когда все подпромисы разрешены в исходном порядке. Вы можете думать об этом как Array.map, но для обещаний.

Как указано в документации Bluebird (библиотека обещаний JS):

Значение выполнения обещания представляет собой массив со значениями выполнения в соответствующих позициях исходного массива.

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

let создает переменную, которая существует внутри этой for итерации. Это означает, что позже в промисе вы получаете доступ к переменной, определенной в этой области итераций, которая не переопределяется следующей итерацией цикла.

person tomb    schedule 03.12.2015