JavaScript | var, let, const

JavaScript | var, let, const

Зачем нужны var, let, const в JavaScript?

В JavaScript для объявления переменных можно использовать слова:

  • var
  • let
  • const

Мы объявляем переменные в среде выполнения кода для того, чтобы иметь возможность обращаться к нужным данным по имени. Например если нам нужно «запомнить» число 777, то мы можем объявить переменную с нужным именем и положить в эту переменную наше заветное число. Далее в коде программы мы уже можем не писать 777, а можем сразу обращаться к этому числу через переменную. И это очень важная концепция в программировании, в целом!

Если мы будем везде просто писать 777 где это нам нужно, то такая программа не будет универсальной. Представьте, что в нашей программе число 777 должно встретиться в 20 местах кода. Если мы однажды захотим поменять 777 на 555, тогда нам нужно будет произвести замену в каждом из 20 мест программы. Это ужасно и скучно. Производя такие замены мы сможем легко ошибиться и в каких-то местах забыть изменить 777 на 555. В результате программа перестанет работать корректно.

Именно для таких ситуаций нужно объявлять переменные — то есть «давать имена» для каких-то уникальных данных.

Когда мы даём имя для переменной, тогда мы фактически создаём участок в оперативной памяти, где будет храниться наше значение. Оперативная память здесь имеет прямое отношение к физическому устройству, на котором работает наша программа.

 

Почему переменные в JavaScript могут объявляться через три способа — через var, let, const?

Каждое из этих слов имеет существенные отличия от других. Каждый способ объявления имени переменной умеет управлять состоянием содержимого имени и самим именем.

Если простым языком, то:

  • «var» обладает максимальной силой. «var» может быть объявлен с одинаковым именем в пределах разных блоков кода. «var» может переобъявить имя переменной (затереть содержимое — данные) в пределах блока и за ним бесконечное количество раз. Данные в объявленной переменной можно переписывать. К имени можно обращаться до объявления.
  • «let» может быть объявлен с одинаковым именем в пределах разных блоков кода. «let» не может переобъявить имя переменной в пределах одного блока. Данные в объявленной переменной можно переписывать. К имени нельзя обращаться до объявления, будет ошибка.
  • «const» может быть объявлен с одинаковым именем в пределах разных блоков кода. «const» не может переобъявить имя переменной в пределах блока. «const» не может переписывать данные в объявленной переменной. К имени нельзя обращаться до объявления, будет ошибка.

Важно отметить, что в самых ранних версиях JavaScript существовал только один способ объявления переменных — через слово «var«. Именно по этой причине оно обладает максимальной силой — максимальными возможностями.

 

Что нужно знать о JavaScript, чтобы лучше понимать механизм объявления переменных?

Нужно иметь представление о том:

  1. Что такое «Ключевые и Зарезервированные слова» в ECMAScript
  2. Что такое «Записи окружающей среды» в ECMAScript
  3. Что такое «Глобальный объект» в ECMAScript

 

«Ключевые и Зарезервированные слова» в ECMAScript

keyword (ключевое слово) — это токен, который соответствует IdentifierName, но также имеет синтаксическое использование; то есть он появляется буквально, шрифтом фиксированной ширины в некоторой синтаксической продукции. Ключевые слова ECMAScript включают if, while, async, await и многие другие.

reserved word (зарезервированное слово) — это IdentifierName, которое нельзя использовать в качестве идентификатора. Многие ключевые слова являются зарезервированными, но некоторые — нет, а некоторые зарезервированы только в определенных контекстах. if и while — это зарезервированные слова. await зарезервировано только внутри асинхронных функций и модулей. async не зарезервировано; его можно использовать в качестве имени переменной или метки оператора без ограничений.

Эта спецификация использует комбинацию грамматических постановок и правил «ранних ошибок«, чтобы указать, какие имена являются действительными идентификаторами, а какие — зарезервированными словами. Все токены в списке ReservedWord ниже, за исключением await и yield, безусловно зарезервированы. Исключения для await и yield указаны в 13.1 с использованием параметризованных синтаксических производств. Наконец, несколько правил «ранних ошибок» ограничивают набор допустимых идентификаторов. См. 13.1.1, 14.3.1.1, 14.7.5.1 и 15.7.1. Таким образом, существует пять категорий имен идентификаторов:

  1. Те, которые всегда разрешены в качестве идентификаторов и не являются ключевыми словами, например Math, window, toString и _;
  2. Те, которые никогда не разрешены в качестве идентификаторов, а именно перечисленные ниже слова ReservedWords, за исключением await и yield;
  3. Те, которые контекстуально разрешены в качестве идентификаторов, а именно await (ждут) и yield (уступают);
  4. Те, которые контекстуально запрещены в качестве идентификаторов, в коде строгого режима: let, static, implements, interface, package, private, protected и public;
  5. Те, которые всегда разрешены в качестве идентификаторов, но также появляются в качестве ключевых слов в определенных синтаксических продуктах в тех местах, где Identifier (идентификатор) не разрешен: as, async, from, get, meta, of, set и target.

Термин conditional keyword (условное ключевое слово) или contextual keyword (контекстное ключевое слово) иногда используется для обозначения ключевых слов, попадающих в последние три категории, и, таким образом, может использоваться в качестве идентификаторов в одних контекстах и в качестве ключевых слов в других.

 

Скриншот из стандарта ECMAScript, который описывает «Ключевые и Зарезервированные слова»:

Раздел Keywords and Reserved Words - ECMAScript - сентябрь 2023
Раздел Keywords and Reserved Words — ECMAScript — сентябрь 2023

 

Записи окружающей среды

Запись среды (Environment Record) — это тип спецификации, используемый для определения ассоциации идентификаторов с конкретными переменными и функциями на основе лексической структуры вложенности кода ECMAScript. Обычно запись среды связана с определённой синтаксической структурой кода ECMAScript, такой как FunctionDeclaration, BlockStatement или предложение Catch в TryStatement. Каждый раз, когда такой код оценивается, создаётся новая запись среды для записи привязок идентификаторов, которые создаются этим кодом.

Каждая запись среды имеет поле [[OuterEnv]], которое либо равно null, либо является ссылкой на внешнюю запись среды. Это используется для моделирования логической вложенности значений Environment Record. Внешняя ссылка (внутренней) записи среды — это ссылка на запись среды, которая логически окружает внутреннюю запись среды. Внешняя запись среды, конечно, может иметь свою собственную внешнюю запись среды. Запись среды может служить внешней средой для множества внутренних записей среды. Например, если FunctionDeclaration содержит два вложенных FunctionDeclaration, тогда записи среды каждой из вложенных функций будут иметь в качестве своей внешней записи среды запись среды текущей оценки окружающей функции.

Записи среды — это чисто механизмы спецификации и не обязательно должны соответствовать какому-либо конкретному артефакту реализации ECMAScript. Программа на ECMAScript не может напрямую обращаться к таким значениям или манипулировать ими.

 

Глобальный объект

Это объект от которого всё создаётся и наследуется в JavaScript. В нём описаны все стандартные классы и их методы, которые старается выучить любой разработчик на JavaScript. Он есть всегда и везде, где работает среда выполнения кода. Без него невозможна разработка.

 

Объявление переменной через «var» в JavaScript

Информация из официальной документации:

Оператор «var» объявляет переменные, которые привязаны к «лексической среде» (LexicalEnvironment) текущего контекста выполнения. Переменные Var создаются при создании экземпляра содержащейся в них «записи среды«(Environment Record) и инициализируются значением undefined при создании. В рамках любой «переменной среды» (VariableEnvironment) общий «идентификатор привязки» (BindingIdentifier) может появляться более чем в одном «объявлении переменной» (VariableDeclaration), но эти объявления вместе определяют только одну переменную. Переменной, определенной с помощью VariableDeclaration с инициализатором Initializer, присваивается значение «выражение присвоения» (AssignmentExpression) его инициализатора Initializer при выполнении VariableDeclaration, а не при создании переменной.

 

Давайте перейдём от документации к практике.

Что будет если объявить переменную через «var» на самом верхнем уровне программы? Имеется ввиду, что в файле «.js» с программой мы объявляем переменную не в каком-то блоке, который оформлен в виде фигурных скобок, а грубо говоря на первой строчке.

var ffff = 'ffff'

Мы создаём имя переменной ffff и сразу же присваиваем строковое значение для этого имени. Для удобства восприятия мы называем имя созвучно с данными.

Что мы получаем при таком объявлении?

Объявили переменную через var и получили ключ у глобального объекта со значением - JavaScript
Объявили переменную через var и получили ключ у глобального объекта со значением — JavaScript

В среде выполнения кода на клиентской стороне мы фактически создаём новый ключ у глобального объекта (объект window) и присваиваем ему значение. Это значит, что теперь данное имя переменной будет видно из любого участка программы двумя способами:

  1. Прямым обращением через «ffff«
  2. Ссылочным обращением через «window.ffff«

Какие тут могут быть проблемы? Если мы используем чьи-то наработки или подключаем сторонние библиотеки, то может сложиться такая ситуация, когда у глобального объекта мы просто перезапишем чужой код своим. После этого часть функционала программы просто перестанет работать.

 

Объявление переменной через «var» в JavaScript умеет выходить из Блоков

Попытка переобъявить имя переменной в Блоке ветвления if приведёт к переписыванию значения в ключе глобального объекта и в самом имени. Это значит, что oбъявление новой переменной через «var» не будет произведено в пределах этого Блока. Границы Блока будут проигнорированы!

window.ffff
//undefined

var ffff = 'ffff'
//undefined

window.ffff
//'ffff'
ffff
//'ffff'
if(true){var ffff = 'q'}
//undefined

ffff
//'q'
window.ffff
//'q'
Переобъявили переменную через var в блоке if и переписали ключ у глобального объекта со значением - JavaScript
Переобъявили переменную через var в блоке if и переписали ключ у глобального объекта со значением — JavaScript

Тут возникает новый логичный вопрос.

 

Чем опасно объявление переменной через «var» в JavaScript?

Внимательно рассмотрите пример работы цикла и объявления переменной, для хранения промежуточных результатов цикла. Мы оборачиваем логику в функцию и затем её вызываем. (Но функция тут не обязательна. Можно было и внутри файла написать.)

function start(){
   for(var i = 0; i < 5; i++){
      console.log('i внутри цикла', i);
   };
   console.log('i после цикла', i);
};
start();

Вывод в лог будет таким:

Переменная i объявленная через var вышла за пределы блока в JavaScript
Переменная i объявленная через var вышла за пределы блока в JavaScript

Данный пример может сбить с толку. По обычной логике любого нормального языка программирования, объявленная переменная «i» должна была оставаться внутри своего Блока, в котором была объявлена. Но конкретно в этом примере с «var» мы можем обратиться к переменной «i» вне её Блока объявления.

Получается, что доступ к переменной, объявленной через «var«, возможен в любом месте тела функции. Границы Блоков становятся «прозрачными» и не влияют на ограничение видимости переменной.

Если бы мы захотели воспользоваться последним значением переменной «i«, то хранимое в нём значение могло бы давать неверный результат. В идеале цикл должен был дойти до числа 4 и на этом закончить свою работу, а второй лог должен был выдать ошибку ссылки на несуществующий идентификатор.

Чтобы такого не происходило, в JavaScript были введены ещё два способа объявления переменных — это «let» и «const«. Они учитывают границы Блоков и не дают возможности заглядывать за их пределы.

 

Что будет с глобальным объектом если объявлять переменные через «let» и «const» в JavaScript на самом верхнем уровне программы?

И вот мы подошли к первому самому важному отличию объявления переменных через «var» от объявлений переменных через «let» и «const«. Я объявлю переменные в новой вкладке браузера (в новой среде выполнения кода), чтобы не путать с предыдущим примером.

let www = 'www'
const eee = 'eee'
//undefined
www
//'www'
eee
//'eee'
window.www
//undefined
window.eee
//undefined

Скриншот из консоли браузера:

Объявление переменных через let или const не создаёт ключей у глобального объекта в JavaScript
Объявление переменных через let или const не создаёт ключей у глобального объекта в JavaScript

Только объявление через «var» даёт нам возможность внести изменения в глобальный объект. Напомню, мы говорим о самом верхнем уровне программы. Поведение объявлений внутри тел функций будет иным — таковы правила.

Мы в любой момент мы можем проверить существование ключей у глобального объекта:

Object.keys(window).includes('www')
//false

Object.keys(window).includes('eee')
//false

Мы видим в обоих случаях false. Значит у глобального объекта нет таких ключей, которые называются www и eee. А если нет ключей, тогда нет и значений для этих ключей.

Объявления через let и const никогда не создают ключи в глобальном объекте - JavaScript
Объявления через let и const никогда не создают ключи в глобальном объекте — JavaScript

Это значит, что к переменным объявленным через «let» и «const» мы можем обращаться только по прямому имени.

 

let и const

Информация из официальной документации:

Объявления let и const определяют переменные, которые привязаны к «лексической среде» (LexicalEnvironment) текущего контекста выполнения. Переменные создаются при создании экземпляра содержащейся в них «записи среды«(Environment Record), но к ним нельзя получить доступ каким-либо образом до тех пор, пока не будет вычислена «лексическая привязка» (LexicalBinding) переменной. Переменной, определенной лексической привязкой LexicalBinding с инициализатором Initializer, присваивается значение AssignmentExpression ее инициализатора Initializer при оценке LexicalBinding, а не при создании переменной. Если LexicalBinding в объявлении let не имеет инициализатора Initializer, переменной при оценке LexicalBinding присваивается значение undefined.

 

Таблица 25: Дополнительные компоненты состояния для контекстов выполнения кода ECMAScript

Component (Компонент) Purpose (Цель)
LexicalEnvironment (Лексическая среда) Идентифицирует запись среды, используемую для разрешения ссылок на идентификаторы, сделанных кодом в этом контексте выполнения.
VariableEnvironment (Переменная среда) Идентифицирует запись среды, которая содержит привязки, созданные VariableStatements в этом контексте выполнения.

Компоненты LexicalEnvironment и VariableEnvironment контекста выполнения всегда являются записями среды.

 

 

 

Что не так со словом «let» в JavaScript?

Важно отметить, что слово «let» не является зарезервированным словом по стандарту ECMAScript. Что это значит для разработчика?

Это значит, что словом «let» можно назвать саму переменную — назвать идентификатор привязки.

Например, мы можем объявлять переменную при помощи зарезервированного слова «var«, а после него написать имя переменной «let«:

var let = 'Вася';

Ошибки не будет! После такого объявления мы сможем обращаться к переменной с именем «let» и получать строку ‘Вася‘.

Слово let в качестве имени переменной в JavaScript
Слово let в качестве имени переменной в JavaScript

Мы сможем использовать переменную «let» в операциях конкатенации с другими строками. Пишем символ плюса и получаем большую строку. Среда выполнения кода чётко понимает, что «let» в нашем случае это не символ объявления переменной, а само название переменной.

Почему такое вообще возможно? Это же прямое нарушение логики работы языка. Как отделять имя переменной от части синтаксиса? С этим вопросом нужно обратиться к разработчикам языка ECMAScript за ответами.

 

Объявление переменной через «const» в JavaScript

Объявление переменной через «const» блокирует возможное переобъявление имени переменной в пределах одного Блока. Конфликтная ситуация связанная с повторным объявлением такого же имени приводит к выбрасыванию ошибки. Неперехваченная синтаксическая ошибка останавливает дальнейшее выполнение кода и весь последующий функционал перестаёт выполняться.

Через «const» нельзя переобъявить имя переменной в пределах блока. Пробуем в файл записать на верхний уровень:

const rrr = 'r';
const rrr = 'r';

В результате получим ошибку «Неперехваченная синтаксическая ошибка: идентификатор ‘rrr’ уже объявлен«.

Ошибка с const - Uncaught SyntaxError- Identifier 'rrr' has already been declared - JavaScript
Ошибка с const — Uncaught SyntaxError- Identifier ‘rrr’ has already been declared — JavaScript

Попытка переобъявить имя переменной через «const» приведёт к ошибке, так как на одном уровне программы уже существует такое объявленное имя переменной.

Через «var» мы также не сможем переобъявить имя переменной. Будет ровно та же ошибка.

const rrr = 'r_const';
var rrr = 'r_var';
console.log(rrr);

Через «let» мы также не сможем переобъявить имя переменной. Будет ровно та же ошибка.

const rrr = 'r_const';
let rrr = 'r_let';
console.log(rrr);

 

 

Мы можем объявить переменные через «const» с такими же именами в пределах разных Блоков.

const rrr = '01_const';
console.log('01', rrr);
if(true){
  const rrr = '02_const';
  console.log('02', rrr);
}
Одинаковое имя переменной объявлено--через const в разных блоках кода - JavaScript
Одинаковое имя переменной объявлено—через const в разных блоках кода — JavaScript

В качестве Блока мы взяли производство «IfStatement». Мы знаем, что существует синтаксические структуры кода ECMAScript, которые умеют создавать изолированные записи среды:

Если по простому, то внутри фигурных скобок Блока IF мы получаем изолированное пространство для объявления переменных. Мы получаем отдельную Запись Среды — вложенную в основную. Это значит, что мы можем создать внутри этого пространства любое имя переменной, которое уже существует в нашей программе вне этого блока. Мы «как-бы переобъявляем» имя для переменной, но фактически этого не делаем.

Что меняется при таком подходе, если одинаковых имён переменных в программе становится несколько? Меняется возможность обращения к одноимённой переменной, которая находится за пределами блока обращения уровнем выше. Уровень ниже никогда не просматривается при обращении (не объявлении) к имени переменной.

Когда мы во втором Блоке обращаемся к rrr в выражении «console.log(’02’, rrr)«, то фактически мы находим объявленную rrr в пределах этого второго Блока. Теперь мы никак не можем дотянуться до rrr из первого Блока и получить строку «01_const«.

 

 

 

Информационные ссылки

JavaScript | Алгоритмические обозначения

JavaScript | Условные обозначения

JavaScript | Зарезервированные слова (ReservedWord)