Переменные — фундаментальное понятие в любом языке программирования. Их способ определения и использования — залог вашего успеха. Каждый программист должен уметь их грамотно использовать: знать правила использования, особенности. В этом руководстве вы узнаете тонкости их определения, области видимости. Также мы познакомимся с важными ключевыми словами JavaScript — var, let, const.

Объявление переменной

В наше время в JavaScript существует три способа объявления переменной — var, let и const. У каждого из них свои особенности. Начнем с простого — таблицы. Она наглядно показывает различия между ними, их особенности.

Если вы не поняли — не переживайте. Обо всех этих понятиях мы поговорим позже.

Область видимости

Область видимости — это часть кода, в которой доступна переменная. В JavaScript два типа области видимости — локальная и глобальная. У локальной области видимости есть несколько видов.

Разберемся с помощью примера — так будет проще понять принцип работы. Допустим, мы объявили переменную message:

const message = 'Hello World'
console.log(message) // 'Hello World'

Что и следовало ожидать — переменная message, вызванная console.log, имеет значение Hello World. Тут вопросов нет, но что будет, если немного поменяем место объявления переменной?

if (true) {
    const message = 'Hello World'
}
console.log(message) // ReferenceError: message is not defined

Упс… Код не работает… Но почему? Суть в том, что оператор if создает локальную,  блоковую область видимости. Мы использовали const — переменная доступна только в этом блоке кода и не может быть вызвана извне.

Поговорим о блоках и функциях подробнее.

Блочная область видимости

Блок — фрагмент кода, который ограничен парой фигурных скобок. При желании ему можно дать имя.

Как было сказано ранее, переменные, объявленные с помощью let и const, видимы только внутри блока. Рассмотрим два похожих примера. В них мы будем использовать разные ключевые слова для создания области видимости:

 const x1 = 1
{
    const x1 = 2
    console.log(x1) // 2
}
console.log(x1) // 1

Этот пример может показаться странным, поэтому разберем его подробнее. Во внешней области видимости мы объявляем переменную x1 со значением 1. После этого мы создаем новую блоковую область видимости с помощью фигурных скобок. Хоть это и странно, но JavaScript это позволяет. В этой области видимости мы объявляем новую переменную, имя которой тоже x1. Не пугайтесь — это совершенно новая переменная, которая доступна только в этом блоке.

Тот же пример, но дадим этой области имя:

const x2 = 1
myNewScope: { // Даем блоку имя
    const x2 = 2
    console.log(x2) // 2
}
console.log(x2) // 1

Пример с while (не пытайтесь его выполнить):

 const x3 = 1
while(x3 === 1) {
    const x3 = 2
    console.log(x3) // 2
}
console.log(x3) // Никогда не выполнится

Что не так с этим кодом? Что будет, если его запустить? Объясняю: x3, объявленная во внешней области, используется для сравнения в строке с while x3 === 1. В любом другом случае я бы присвоил x3 новое значение и вышел бы из цикла while. Но ведь мы объявили х3 внутри блока и не можем изменить глобальную х3. То есть, условие while всегда будет истинным — цикл бесконечен. Из-за этого ваш браузер зависнет. Если вы используете терминал для запуска кода с помощью Node.Js, то он напечатает много 2.

Исправить этот код довольно сложно. Но решение есть — переименовать переменные.

До сих пор мы использовали лишь const, но то же самое произошло бы и с let. Как вы могли заметить, у var тоже функциональная область видимости. Но что же это значит? Давайте посмотрим:

var x4 = 1
{
    var x4 = 2
    console.log(x4) // 2
}
console.log(x4) // 2

Отлично! Хоть мы и объявили переменную x4 внутри функции, но ее значение изменилось и в глобальной области видимости. Это одно из самых важных отличий между let, const и var. Обычно подобные вопросы любят задавать на собеседованиях.

Функциональная область видимости

Функциональная область видимости — разновидность блочной. Здесь let и const будут вести себя так же, как в предыдущих примерах. Функциональная область инкапсулирует переменные, если используется var. Давайте рассмотрим примеры с xn.

Пример с const и let

const x5 = 1
function myFunction() {
    const x5 = 2
    console.log(x5) // 2
}
myFunction()
console.log(x5) // 1

А теперь перейдем к var

var x6 = 1
function myFunction() {
    var x6 = 2
    console.log(x6) // 2
}
myFunction()
console.log(x6) // 1

В данном случае var работает так же, как let и const.

function myFunction() {
    var x7 = 1
}
console.log(x7) // ReferenceError: x7 is not defined

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

Но это еще не все! JS развивается, и постепенно появляются новые типы областей видимости.

Модульная область видимости

Цель введения модулей в ES6 — необходимость в том, чтобы переменные в одном модуле не влияли на переменные в другом. Представьте, в котором импортированные модули из библиотеки будут конфликтовать с вашими переменными? JS не такой уж и беспорядочный! Модули создают свою собственную область видимости, которая инкапсулирует все переменные, созданные с помощью var, let или const. То есть, аналогично функциональной области видимости.

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

Мы поговорили о различных типах локальных областей видимости, теперь давайте перейдем к глобальным.

Глобальная область видимости

Переменные, объявленные вне функции, блока или модуля — глобальные. Глобальные переменные могут вызываться в любом фрагменте кода.

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

А как бы вы объявили глобальную переменную? Это зависит от контекста — в браузере она объявляется не так, как в приложении Node.Js. В случае с браузером это делается просто:

<script>
let MESSAGE = 'Hello World'
console.log(MESSAGE)
</script>

Или с помощью window-объекта:

<script>
window.MESSAGE = 'Hello World'
console.log(MESSAGE)
</script>

Скорее всего, вы бы поступили именно так. Но с глобальными переменными нужно быть осторожным.

Вложенные области видимости

Как вы могли догадаться, области видимости могут быть вложенными. То есть, внутри области видимости может быть еще одна — это нормально. Сделать мы это можем простым добавлением оператора if внутри функции. Пример:

function nextedScopes() {
    const message = 'Hello World!'
    if (true) {
        const fromIf = 'Hello If Block!'
        console.log(message) // Hello World!
    }
    console.log(fromIf) // ReferenceError: fromIf is not defined
}
nextedScopes()

Лексическая область видимость

В каком-то смысле мы уже использовали лексическую область видимости, но не знали об этом. Если дочерние области видимости имеют доступ к переменным, которые определены во внешней — это лексическая область видимости.

Пример:

 function outerScope() {
    var name = 'Juan'
    function innerScope() {
        console.log(name) // 'Juan'
    }
    return innerScope
}
const inner = outerScope()
inner()

Это может показаться сложным, но это не так. В функции externalScope объявляется переменная name со значением Juan и функция с именем innerScope. В innerScope не объявляется никаких переменных, но она может использовать name, которая находится во внешней области видимости.

Когда вызывается outerScope(), она возвращает ссылку на innerScope, которая позже вызывается из внешней области. При первом чтении этого кода в вы можете быть сбиты с толку — почему innerScope выводит с помощью console.log значение Juan? Мы ведь вызываем ее из глобальной или модульной области видимости, где name не объявлена.

Работает это благодаря замыканию. Замыкание — отдельная тема, детальнее о ней вы можете узнать здесь.

Что такое подъем

В терминологии JavaScript подъем означает, что переменная записывается в память еще на стадии компиляции. То есть переменные могут быть использованы до их объявления. Звучит запутанно, лучше посмотреть на пример.

 function displayName(name) {
    console.log(name)
}
displayName('Juan')

Вывод
'Juan'

Отлично! Как и ожидалось, это работает. Но что думаете про этот код?

 hoistedDisplayName('Juan')
function hoistedDisplayName(name) {
    console.log(name)
}

Вывод
'Juan'

Стоп-стоп-стоп… что? Как бы безумно это ни звучало, но функция загружается в  память еще до того, как код запускается. Функция hoistedDisplayName доступна до ее определения — по крайней мере на первый взгляд.

Этим свойством обладают не только функции, но и переменные, объявленные с помощью var. Давайте посмотрим на пример:

console.log(x8) // неопределено
var x8 = 'Hello World!'

Не то, что вы думали? Если переменная x8 загружается в память еще до ее появления в коде, то это не значит, что ей присваивается значение. Именно во время вызова console.log (x8) мы не получаем ошибку «переменная не объявлена». В этом случае ее тип будет undefined. А что будет, если использовать let или const? Как вы помните, в нашей таблице это свойство отсутствует.

console.log(x9) // Cannot access 'x9' before initialization
const x9 = 'Hello World!'

Возникает ошибка.

Подъем — не самая известная фишка JavaScript, но она очень важна. Убедитесь в том, что вы поняли разницу между let, const и var — этот вопрос можно встретить на собеседовании.

Повторное определение переменных

В этом разделе мы поговорим о переменных, объявленных с помощью const. Переменные, объявленные с помощью const, неизменяемы — мы не можем изменить их значение. Но есть пара хитростей. Вот парочка примеров:

const c1 = 'hello world!'
c1 = 'Hello World' // TypeError: Assignment to constant variable.

Как и ожидалось, значение мы изменить не можем. Или можем?

const c2 = { name: 'Juan' }
console.log(c2.name) // 'Juan'
c2.name = 'Gera'
console.log(c2.name) // 'Gera'

Мы только что изменили значение переменной, объявленной через const? Если кратко — нет. Переменная c2 ссылается на объект со свойством name.c2 — это ссылка на этот объект, это его значение. Когда мы используем c2.name, мы берем указатель на c2 и получаем доступ к значению. То, что мы меняем, когда выполняем строку с c2.name — это значение свойства name в объекте, но не ссылку, хранящаяся в c2. То есть, c2 остается неизменной, но значение свойства теперь другое.

Смотрите, что происходит, когда мы пытаемся обновить значение другим способом:

const c3 = { name: 'Juan' }
console.log(c3.name) // 'Juan'
c3 = { name: 'Gera' } // TypeError: Assignment to constant variable.
console.log(c3.name)

Объект выглядит так же, но мы создаем новый объект { name: 'Gera' } и пытаемся назначить этот новый объект вместо c3. Но сделать мы этого не можем — это const.

Перевод статьи: Understanding Variables, Scope, and Hoisting in JavaScript