Объяснение `let` и блокировка области с помощью циклов

Я понимаю, что 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.

Ответ 1

Является ли это просто синтаксическим сахаром для ES6?

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

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

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

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

Ваш оператор цикла 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)) } делает "desugar" более сложным

// 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++;
          …

Ответ 2

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

объяснение MDN также поддерживает это:

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

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

Короче говоря, при использовании 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);
}

Ответ 3

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

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

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

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

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

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

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

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

Ответ 4

В JavaScript ключевое слово let определяет переменную уровня уровня блока, в отличие от ключевого слова var, который определяет глобальную переменную или переменную уровня области.

Я не понимаю, как с помощью let вы получаете 0-9, он должен записывать 0 бесконечность раз!