Объяснение `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.