Объяснение `let` и блока scoop с циклами for

Я это понимаю let предотвращает дублирование объявлений, что приятно.

let x;
let x; // error!

Переменные, объявленные с let может также использоваться в затворах, которые можно ожидать

let i = 100;
setTimeout(function () { console.log(i) }, i); // '100' after 100 ms

Что мне трудно понять, так это то, как let относится к петлям. Это, кажется, характерно для for петли. Рассмотрим классическую проблему:

// prints '10' 10 times
for (var i = 0; i < 10; i++) { process.nextTick(_ => console.log(i)) }
// prints '0' through '9'
for (let i = 0; i < 10; i++) { process.nextTick(_ => console.log(i)) }

Почему используется let в этом контексте работаешь? В моем воображении, хотя виден только один блок, for на самом деле создает отдельный блок для каждой итерации и let объявление делается внутри этого блока... но есть только один let декларация для инициализации значения. Это просто синтаксический сахар для ES6? Как это работает?

Я понимаю разницу между var а также let и проиллюстрировали их выше. Мне особенно интересно понять, почему разные объявления приводят к разным выводам с использованием for петля.

6 ответов

Решение

Это просто синтаксический сахар для ES6?

Нет, это больше, чем синтаксический сахар. Кровавые подробности похоронены в §13.6.3.9 CreatePerIterationEnvironment,

Как это работает?

Если вы используете это let Ключевое слово в for оператор, он проверит, какие имена он связывает, а затем

  • создать новую лексическую среду с этими именами для а) выражения инициализатора б) каждой итерации (прежде всего для вычисления выражения приращения)
  • скопировать значения из всех переменных с этими именами из одной в следующую среду

Ваш цикл заявления for (var i = 0; i < 10; i++) { process.nextTick(_ => console.log(i)) } Desugars к простому

// omitting braces when they don't introduce a block
var i;
i = 0;
if (i < 10)
    process.nextTick(_ => console.log(i))
    i++;
    if (i < 10)
        process.nextTick(_ => console.log(i))
        i++;
        …

в то время как for (let i = 0; i < 10; i++) { process.nextTick(_ => console.log(i)) } делает "десугар" намного сложнее

// using braces to explicitly denote block scopes,
// using indentation for control flow
{ let i;
  i = 0;
  __status = {i};
}
{ let {i} = __status;
  if (i < 10)
      process.nextTick(_ => console.log(i))
      __status = {i};
}   { let {i} = __status;
      i++;
      if (i < 10)
          process.nextTick(_ => console.log(i))
          __status = {i};
    }   { let {i} = __status;
          i++;
          …

Я нашел это объяснение из книги " Изучение ES6" лучшим:

Объявление переменной в заголовке цикла for создает единственное связывание (пространство хранения) для этой переменной:

const arr = [];
for (var i=0; i < 3; i++) {
    arr.push(() => i);
}
arr.map(x => x()); // [3,3,3]

Каждое i в теле трех функций-стрелок относится к одной и той же привязке, поэтому все они возвращают одно и то же значение.

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

const arr = [];
for (let i=0; i < 3; i++) {
    arr.push(() => i);
}

arr.map(x => x()); // [0,1,2]

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

let представляет область видимости блока и эквивалентное связывание, так же как функции создают область с замыканием. Я полагаю, что соответствующий раздел спецификации - 13.2.1, где в примечании упоминается, что let Объявления являются частью LexicalBinding и оба живут в Lexical Environment. Раздел 13.2.2 гласит, что var объявления присоединяются к VariableEnvironment, а не к LexicalBinding.

Объяснение MDN также поддерживает это, заявляя, что:

Он работает, связывая ноль или более переменных в лексической области одного блока кода

Предполагается, что переменные связаны с блоком, который варьируется на каждой итерации, требующей нового LexicalBinding (я полагаю, не 100% в этой точке), а не окружающей Lexical Environment или VariableEnvironment, которая была бы постоянной для продолжительности вызова.

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

Адаптируем ваш пример для запуска в браузере:

// prints '10' 10 times
for (var i = 0; i < 10; i++) {
  setTimeout(_ => console.log('var', i), 0);
}

// prints '0' through '9'
for (let i = 0; i < 10; i++) {
  setTimeout(_ => console.log('let', i), 0);
}

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

for (var i = 0; i < 10; i++) {
  setTimeout(function(_) {
    return console.log(i);
  }, 0);
}

var _loop = function(_i) {
  setTimeout(function(_) {
    return console.log(_i);
  }, 0);
};

// prints '0' through '9'
for (var _i = 0; _i < 10; _i++) {
  _loop(_i);
}

Предполагая, что Бабель достаточно соответствует, это соответствует моей интерпретации спецификации.

Давайте посмотрим на «let» и «var» с setTimeout, которые в основном задавали в интервью.

      (function timer() { 
   for (var i=0; i<=2; i++) 
       { setTimeout(function clog() {console.log(i)}, i*1000); } 
    })();

(function timer() { 
   for (let i=0; i<=2; i++) 
       { setTimeout(function clog() {console.log(i)}, i*1000); } 
    })();

Давайте подробно рассмотрим, как этот код выполняется в компиляторе javascript.Ответ для «var» — «222» из-за функциональной области, а для «let» — «012», потому что это блочная область.

Теперь давайте подробно рассмотрим, как это выглядит при компиляции для «var». (Это немного сложнее объяснить по коду, чем по аудио или видео, но я изо всех сил стараюсь дать вам.)

      var i = 0;

if(i <=2){
setTimeout(() => console.log(i));
}
i++;  // here the value of "i" will be 1

if(i <=2){
setTimeout(() => console.log(i));
}
i++;   // here the value of "i" will be 2

if(i <=2){
setTimeout(() => console.log(i));
}
i++;  // here the value of "i" will be 3

После того, как код наконец будет выполнен, он напечатает весь console.log, где значение «i» равно 6. Таким образом, окончательный вывод: 222

В «пусть я» будет объявлено в каждой области. Здесь следует отметить точку импорта: «i» получит значение из предыдущей области , а не из объявления. (Приведенный ниже код является просто примером того, как он выглядит в компиляторе, и попытка его не сработает)

      {
    //Scope  1
    { 
    let i;  
    i= 0;
    
    
    if(i<=2) {
        setTimeout(function clog() {console.log(i)};);
    }
    i++;   // Here "i" will be increated to 1
    
    }
    
    //Scope 2  
    // Second Interation run
    {
    let i;
    i=0;
    
        // Even “i” is declared here i= 0 but it will take the value from the previous scope
    // Here "i" take the value from the previous scope as 1
    if(i<=2) {    
        setTimeout(function clog() {console.log(i)}; );
    }
    
    i++;   // Here “i” will be increased to 2
    
    }
    
    
    //Scope 3 
    // Second Interation run
    {
    let i;
    i=0;
    
    // Here "i" take the value from the previous scope as 2
    if(i<=2) {   
        setTimeout(function clog() {console.log(i)}; );
    }
    
    i++;   // Here "i" will be increated to 3
    
    }
    

}

Таким образом, он будет печатать значение «012» в соответствии с областью блока.

Недавно я тоже запутался в этой проблеме. Согласно приведенным выше ответам, вот мое понимание:

      for (let i=0;i<n;i++)
{
   //loop code
}

эквивалентно

      // initial
{
    let i=0
}
// loop
{
    // Sugar: For-Let help you to redefine i for binding it into current block scope
    let i=__i_value_from_last_loop__

    if (i<=n){
        //loop code
    }
    i++
}

Let — область действия блока. Доступ к переменной, объявленной внутри цикла for, можно получить даже вне цикла for, потому что var — это только область действия функции. Вы не можете получить доступ к var, определенному внутри функции, извне. С каждой итерацией создается новый Let. Но поскольку var является областью действия функции, и она доступна вне цикла for, она становится глобальной, и с каждой итерацией обновляется одна и та же переменная var.

Другие вопросы по тегам