У цикла for существует 3 производства:
Мы видим, что внутри круглых скобок существует 3 зоны, которые отделены точкой с запятой «;«.
Каждая зона имеет свой смысл. Но нас интересует первая зона.
В ней можно разместить:
- не let и квадратная скобка [
- var
- LexicalDeclaration и Expression
Рассмотрим третий вариант производства, который может включать «LexicalDeclaration» и «Expression«.
Оно в свою очередь состоит из «LetOrConst BindingList«.
Из стандарта:
Объявления let и const определяют переменные, область действия которых ограничена LexicalEnvironment текущего контекста выполнения. Переменные создаются при создании экземпляра содержащей их записи среды, но к ним нельзя получить доступ каким-либо образом, пока не будет оценена LexicalBinding переменной. Переменной, определенной LexicalBinding с Initializer, присваивается значение AssignmentExpression её Initializer при оценке LexicalBinding, а не при создании переменной. Если LexicalBinding в объявлении let не имеет Initializer, переменной присваивается значение undefined при оценке LexicalBinding.
Примеры использования
Будем писать функции. Начнём с простого примера, чтобы понять «боль» и «печаль»:
function f1(){for(a=0; a<10; a++){console.log(a)}}
Вывод в консоли браузера:
Что здесь не так?
Мы объявили функцию. Мы её вызвали. Цикл успешно сделал 10 выводов. И вроде всё логично складывается. Инкремент тоже отрабатывает нормально. Но.
Но проблема в том, что переменная «a» записанная в параметрах цикла, прописалась в качестве ключа у глобального объекта window среды выполнения кода (в браузере). Это значит, что она видна отовсюду в нашей программе и её значение может изменить в любой момент какая-угодно функция. Рассчитывать на эту переменную больше нельзя так как в неё может попасть что угодно.
Где же прописалась наша переменная? Давайте смотреть на скрин:
А теперь давайте напишем ещё одну функцию с циклом, где также будем использовать нашу «a» как инкрементируемое значение, только в этот раз также запишем переменную «b«.
function f2(){for(b=0; b<3; a++, b++){console.log(a+b)}}
Смотрим снова в консоль
И вот наша вторая функция уже вовсю эксплуатирует значение ключа «a«. И она не просто его получает, а ещё и меняет.
То есть эти две функции не работают изолированно друг от друга. Циклы в их телах не создают ограничения области действия для идентификаторов. Такой подход может однажды «выстрелить в ногу». Подсчитывать важные данные такими функция просто невозможно.
Как изолировать параметры цикла от других участков программы?
Нам нужны локальные переменные, которые будут участвовать в параметрах цикла. И эти локальные переменные нельзя будет изменить извне.
Давайте создадим функцию, похожую на первую, но в этот раз объявим переменную «a» прям в параметре цикла.
function f3(){for(let a=0; a<10; a++){console.log(a)}}
Вызываем и смотрим:
И вот сейчас всё работает как нужно. Третья функция не изменяет ключ «a» у глобального объекта window. Его значение по-прежнему 13. В свою очередь глобальный объект ничего не знает о локальной переменной цикла, которая была объявлена через ключевое слово let.
Мы добились изоляции, а значит можем быть более уверенны в работе наших программ.
Несколько операторов let в блоке параметров цикла
Возникает резонный вопрос «А что если у нас несколько переменных должны участвовать в цикле? Как их сделать изолированными?»
Первое, что хочется придумать — это записать несколько операторов let через запятую до первого символа «;» из блока параметров цикла.
Давайте попробуем:
function f4(){for(let a=0, let b=0; a<10; a++){console.log(a)}}
И у нас ничего не получается.
Текст ошибки:
Uncaught SyntaxError: let is disallowed as a lexically bound name
Перевод текста ошибки:
Неперехваченная синтаксическая ошибка: let запрещено как лексически связанное имя
Говоря простым языком, мы должны отделять команды при помощи символа точки с запятой — «;«. Нельзя через одну запятую написать слово (идентификатор привязки), которое зарезервировано языком JavaScript.
В нашем случае синтаксический анализатор сначала смотрит на первое ключевое слово let в блоке параметров. Затем, когда он видит первую запятую, он ожидает получить после неё второе имя идентификатора привязки, а вместо этого видит зарезервированное ключевое слово let, что противоречит перечисляемым идентификаторам привязки.
Понять это можно на примере. Абстрагируемся от цикла. В общем коде программы возможны 2 формы записи:
// ПЕРВАЯ форма записи let
let a; let b; let c;
Мы объявили 3 переменные (a, b и c) и не присвоили им никаких значений. Переменные доступны, но отвечают они undefined
Переменные (fj, gh, lk) небыли объявлены и поэтому мы получаем ошибку ссылки на них.
// ВТОРАЯ форма записи let
let a1, b1, c1;
Переменные (a1, b1, c1) теперь есть в среде выполнения кода, а переменных (q1, q2, q3) нет.
Теперь вернёмся к нашему циклу. У него всё тоже самое. Когда мы напишем ключевое слово let, тогда он будет ожидать перечисления через запятую новых идентификаторов привязки, которые будут работать только внутри цикла. Это очень похоже на вторую форму записи объявлений переменных через let.
function f5(){for(let a=0, b=101, c=2002; a<10; a++, b++, c++){console.log(a, b, c)}}
Смотрим результат выполнения
Мы записали одно слово let и после него через запятую перечислили имена идентификаторов с их выражениями присваивания до первой точки с запятой «;»
Это говорит о том, что в нашем цикле сейчас существуют 3 независимые изолированные переменные, которые объявлены через let. КАЖДЫЙ ОБЪЯВЛЕН ЧЕРЕЗ LET, хоть это и не заметно на первый взгляд.
Мы можем обратиться к нашим старым глобальным переменным «a» и «b» и убедиться, что их значения никак не изменились.
А самое важное то, что запись «b=101» НЕ ПЕРЕТЁРЛА значение у глобальной переменной «b«. Цикловая «b» стала равной 110, а глобальная «b» по-прежнему 3. Просто отсутствие let может первое время путать и вводить в заблуждение.
Информационные ссылки
Стандарт ECMAScript — https://tc39.es/ecma262/multipage/