Почему переменная становится недоступной?

В чем причина различного поведения кода при добавлении условия внутри функции обратного вызова `setTimeout`, если при этом использование `condition === 0` приводит к ошибке "Uncaught ReferenceError: Cannot access 'testValue' before initialization", в то время как замена этого условия на `true` или использование самовызывающейся функции не вызывает ошибок? 

Вот код, который демонстрирует эту проблему: 
<pre><code class="javascript">let testValue = 123;

function testFn() {
    let condition = 0;
    let timer = setTimeout(() =&gt; {
        if (condition === 0) {
            console.log(testValue);
        }
    });
}

testFn(); //123</code></pre> 

После первой попытки все работает корректно, но при повторных запусках появляется ошибка. В случае, когда вместо условия использовано `true`, код работает нормально:
<pre><code class="javascript">let testValue = 123;

function testFn() {
    let condition = 1;
    let timer = setTimeout(() =&gt; {
        if (true) {
            console.log(testValue);
        }
    });
}

testFn(); //123</code></pre> 

Также код работает корректно, если использовать самовызывающуюся функцию:
<pre><code class="javascript">let testValue = 123;

function testFn() {
    let condition = 1;
    let timer = (() =&gt; {
        if (condition === 1) {
            console.log(testValue);
        }
    })();
}

testFn(); //123</code></pre> 

Кроме того, если поместить весь код в самовызывающуюся функцию, ошибки также не возникает:
<pre><code class="javascript">(()=&gt;{
    let testValue = 123;

    function testFn() {
        let condition = 1;
        ...
    }
})();</code></pre>
  • 31 января 2025 г. 17:14
Ответы на вопрос 2
Проблема, с которой вы столкнулись, связана с областью видимости и подъемом переменных в JavaScript, особенно при использовании стрелочных функций и конструкций, связанных с `setTimeout`.

Давайте по порядку разберем код и ваши наблюдения:

1. **Ошибка "Uncaught ReferenceError: Cannot access 'testValue' before initialization"**:

   Эта ошибка возникает, когда вы пытаетесь обратиться к переменной, определенной через `let` или `const`, до того, как она была инициализирована. Область видимости переменных, объявленных с использованием `let` или `const`, ограничена блоком, в котором они были определены.

   В вашем случае это происходит потому, что при повторном вызове функции `testFn` переменная `testValue` инициализируется в каждой новой функции. Однако, если в `setTimeout` используется условие `if (condition === 0)`, это приводит к тому, что вызывается ошибка, если `condition` не соответствует этому значению, а доступ к `testValue` станет возможным только после того, как выполнение функции дойдет до области видимости, где `testValue` доступна. Таким образом, если `condition` равен 0, код никогда не дойдет до точки, где `testValue` будет инициализирован, и будет зафиксирована ошибка.

2. **Использование `true` или самовызывающейся функции**:

   Когда вы заменяете условие `if (condition === 0)` на `if (true)`, вы позволяете коду всегда обращаться к `testValue`, и поэтому ошибка не возникает. В этом случае переменная доступна в пределах функции обратного вызова `setTimeout`.

   В случае самовызывающейся функции код выполняется сразу же, и поэтому все переменные инициализируются до обращения к `testValue`. Таким образом, в момент вызова `console.log(testValue)` все необходимые переменные уже инициализированы и доступны.

3. **Общая рекомендация**:

   Чтобы избежать подобных ошибок, всегда следите за областью видимости переменных и их инициализацией в асинхронных вызовах, таких как `setTimeout`, особенно если вы используете конструкции, которые могут привести к изменению контекста исполнения. Иногда может помочь использование замыканий, чтобы сохранить состояние переменных на момент их инициализации, или же просто избегать ссылки на переменные, которые могут быть не инициализированы, в асинхронных контекстах.

Вот пример исправленного кода, чтобы избежать ошибок:

```javascript
let testValue = 123;

function testFn() {
    let condition = 1; // Измените значение на 1 для правил
    let timer = setTimeout(() => {
        if (condition === 1) { // Убедитесь, что значение корректное
            console.log(testValue);
        }
    }, 1000); // Добавил задержку, чтобы было видно, как это работает
}

testFn(); // 123
```

Эти изменения обеспечивают, что в момент выполнения функции обратного вызова доступ к `testValue` всегда будет возможен, и ошибки больше не появятся.
Похоже на баг/фичу консоли Google Chrome. 
В консоли FireFox повторные запуски корректно ругаются на переобъявление let testValue

Так действительно делать нельзя, но разрабы Chrome с версии 80 решили «помочь» разработчикам, использующим консоль, и ленящимся заключать весь код в фигурные скобки, чтобы изолировать область видимости. С FireFox такая привычка у меня лично уже выработалась, все эксперименты в консоли – внутри { }

И вот в консоли Хрома теперь можно переназначать и переенные let , и даже const . И классы. Печаль.

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

TL&DR; предлагаю писать корректный JS в строгих песочницах, где два let подряд не пройдут.
Похожие вопросы