Объяснение `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
бесконечность раз!