Зарегистрируйтесь, чтобы продолжить обучение

Область видимости и замыкания JS: Функции

Упрощенная визуализация областей видимости и окружений в javascript

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

В программировании такая изоляция называется областью видимости. В этом уроке мы поговорим об окружении. Вы узнаете, на какие зоны делятся программы и как это влияет на работу кода.

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

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

  • Глобальную (внешнюю)
  • Локальную (внутреннюю)

В глобальной области видимости находится все, что мы создаем снаружи функций, инструкций 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 — это и есть замыкание. Другими словами, это сочетание функции и окружения, где она была задана. Функция замкнула в себе некоторую информацию из области видимости.

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

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


Дополнительные материалы

  1. Variables and scoping / Exploring JS
  2. Scope / Wikipedia
  3. Closure / Wikipedia
  4. Замыкания в JS / Mozilla Developer Network

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff