Если вы построите химический завод, неплохо бы изолировать его от окружающего мира, чтобы не произошла утечка химикатов. Можно сказать, что в этом здании свое окружение — микроклимат, изолированный от внешней окружающей среды.
В программировании такая изоляция называется областью видимости. В этом уроке мы поговорим об окружении. Вы узнаете, на какие зоны делятся программы и как это влияет на работу кода.
Области видимости
С точки зрения окружения, программы делятся на две области видимости:
- Глобальную (внешнюю)
- Локальную (внутреннюю)
В глобальной области видимости находится все, что мы создаем снаружи функций, инструкций if
, циклов и других блоков кода:
const age = 29
const multiplier = (num) => {
const x = 10
return num * x
}
let result = true
В этом примере константа age
, функция multiplier
и переменная result
имеют глобальную область видимости.
Поговорим подробнее о локальной зоне видимости из этого примера. Внутри функции multiplier
есть константа x
. Она находится внутри блока кода, она видна только внутри этой функции.
В функции multiplier
есть еще один компонент из локальной области видимости — аргумент num
. Он не задан так же четко, как константы или переменные, но ведет себя почти как локальная переменная.
У нас нет доступа к x
снаружи, как будто ее там не существует:
const multiplier = (num) => {
const x = 10
return num * x
}
console.log(x) // ReferenceError: x is not defined
В этом примере console.log
вызывается в глобальном окружении, но при этом x
не задан глобально. Поэтому мы получаем сообщение Reference Error
.
Попробуем задать x
глобально:
const x = 55
const multiplier = (num) => {
const x = 10
return num * x
}
console.log(x) // 55
Теперь существует глобальный x
. Мы вывели его значение на экран, но локальный x
внутри функции multiplier
по-прежнему виден только внутри этой функции.
Эти два x
не имеют ничего общего друг с другом, они находятся в разных областях видимости. Они не схлопываются в одно целое, несмотря на одинаковое имя.
Любой блок кода между фигурными скобками имеет локальную область видимости. Вот пример с блоком if
:
let a = 0
if (a === 0) {
const local = 10
}
console.log(local) // => ReferenceError: local is not defined
То же работает для циклов while
и for
.
Теперь мы знаем, что локальное недоступно снаружи, а глобальное — доступно везде. Даже внутри чего-то? Да, и это видно в примере ниже:
let a = 0
const changer = () => {
a++
}
console.log(a) // 0
changer()
console.log(a) // 1
Эта глобальная переменная a
изменилась внутри функции changer
. Функция выполняет что-то, только когда ее вызывают, а не когда ее определяют. Поэтому вначале a
это 0
, но после вызова changer
переменная a
становится 1
.
Может показаться, что было бы удобно все поместить в глобальную область видимости и забыть о сложностях. На самом деле, это ужасная практика. Глобальные переменные делают код невероятно хрупким. В таком случае может сломаться все что угодно. Поэтому избегайте глобальной области видимости — храните вещи там, где им место.
Лексическая область видимости
Кроме глобальной и локальной, существует еще и лексическая область видимости. Это конкретный механизм, одно из правил для области видимости, которое применяется в JavaScript и большинстве других языков.
Под лексической областью видимости можно понимать просто механизм поиска значений: смотрим в текущей области, если нет — идем на уровень выше, и так далее. Слово «лексический» означает, что видимость задается исключительно текстом программы.
Другими словами, мы можем, посмотреть на текст программы и узнать область видимости в любой точке. В других языках может быть не лексический механизм, а динамический.
Разберемся, как лексическая область работает на практике. Взгляните на эту программу:
let a = 7
let b = 10
const multiplier = () => {
let a = 5
return a * b
}
multiplier() // 50
Функция multiplier
возвращает произведение a
и b
. Значение a
задано внутри, а b
— нет.
Пытаясь решить умножение a * b
, JavaScript ищет значения a
и b
. Он начинает искать локально и выходит наружу по одной области видимости за шаг. Так происходит до тех пор, пока JavaScript:
- Не найдет то, что нужно
- Не увидит, что это невозможно найти
В этом примере JavaScript начинает с поиска a
внутри локальной области видимости — внутри функции multiplier
. Он находит значение сразу и переходит к b
. Невозможно найти значение b
в локальной области видимости, поэтому он переходит к внешней области. Тут он находит b
— это 10
. Таким образом a * b
превращается в 5 * 10
, а затем — в 50
.
Весь этот кусок кода может быть внутри другой функции, и еще внутри другой функции. Если бы b
не нашлась здесь, JavaScript продолжил бы искать b
за пределами функции, слой за слоем.
Заметьте, что a = 7
не затрагивает вычисления. Значение a
нашлось внутри, поэтому внешняя a
не сыграла роли.
Этот механизм называется лексической областью видимости. Область видимости любого компонента определяется его расположением внутри кода. И вложенные блоки имеют доступ к их внешним областям видимости.
Замыкание
Большинство языков программирования имеют что-то вроде области видимости или окружения, и этот механизм позволяет существовать замыканиям.
Замыкание — это сочетание функции и окружения, где она была заявлена. Другими словами, это всего лишь новое название функции, которая запоминает внешние штуки, используемые внутри.
Давайте вспомним, как функции создаются и используются:
const f = () => {
return 0
}
Функция f
довольно бесполезная, она всегда возвращает 0
. Весь этот набор состоит из двух частей:
- Константы
- Самой функции
Важно помнить, что эти два компонента раздельны. Первый — константа с именем f
. Ее значение могло бы быть числом или строкой, но здесь это функция.
Когда мы вызываем эту функцию, это выглядит вот так:
f()
Вернемся к замыканиям и рассмотрим следующий код:
const createPrint = () => {
const name = 'King'
const printName = () => {
console.log(name)
}
return printName
}
const myPrint = createPrint()
myPrint() // King
Функция createPrint
создает константу name
и затем функцию с именем printName
. Созданные функция и константа локальны — они доступны только внутри createPrint
.
У самой printName
нет локальных компонентов. Но у нее есть доступ к своей области видимости — то есть к внешней области, где задана константа name
.
Затем функция createPrint
возвращает функцию printName
. Помните, что определения функций — это описания запущенных функций, просто фрагменты информации, как числа или строки. Поэтому мы можем вернуть определение функции, как мы возвращаем число.
Во внешней области видимости мы создаем константу myPrint
и задаем ей значение, которое возвращает вызов функции createPrint()
. Этот вызов возвращает функцию, так что теперь myPrint
— это функция. Вызовите ее, и на экран выведется King
.
Тут есть одна странная штука: эта константа name
была создана внутри функции createPrint
. Функция была вызвана и исполнена. Как мы знаем, когда функция заканчивает работу, она больше не существует.
Этот магический ящик исчезает со всеми своими внутренностями, но он возвращает другую функцию, и уже она запоминает константу name
. Поэтому когда мы вызывали myPrint()
, она вывела King
— запомненное значение. При этом больше не существует та область видимости, где мы задали это значение.
Функция, которую мы вернули из createPrint
— это и есть замыкание. Другими словами, это сочетание функции и окружения, где она была задана. Функция замкнула в себе некоторую информацию из области видимости.
Если использовать их разумно, замыкания могут сделать код приятней, чище и проще для чтения. Даже сама идея возврата функций тем же способом, которым можно возвращать числа и строки, дает больше возможностей и гибкости.
Вы заметите, как часто эти идеи используются в программировании. Мы рассмотрим их потенциал в следующих курсах.
Дополнительные материалы
- Variables and scoping / Exploring JS
- Scope / Wikipedia
- Closure / Wikipedia
- Замыкания в JS / Mozilla Developer Network
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.