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

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

Область видимости в JavaScript определяет, к каким переменным у вас есть доступ. Есть два вида областей видимости - глобальная и локальная.

Глобальная область видимости (global scope)

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

const globalVariable = 'some value'

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

const hello = 'Привет, мир!'

function sayHello () {
  console.log(hello)
}

console.log(hello)  // => 'Привет, мир!'
sayHello()  // => 'Привет, мир!'

Несмотря на то, что вы можете объявлять переменные в глобальной области видимости, этого делать не рекомендуется. Это связано с тем, что существует вероятность столкновения имен, когда две или более переменных имеют одинаковое название. А если вы объявили свои переменные с помощью const или let, вы получите сообщение об ошибке всякий раз, когда произойдет конфликт имен, что нежелательно.

// Так делать не стоит!
let thing = 'something'
let thing = 'something else' // Ошибка, т.к. thing уже объявлена

Если вы объявляете свои переменные с помощью var, ваша вторая переменная перезапишет первую после того, как та была объявлена. Это также нежелательно, поскольку этим вы затрудняете отладку кода.

// Валидно, но лучше подобного избегать!
var thing = 'something'
var thing = 'something else'
console.log(thing)  // => 'something else'

Подытоживая, рекомендуется объявлять переменные локально, а не глобально.

Локальная область видимости (local scope)

Переменные, которые можно использовать только в определенной части вашего кода, считаются находящимися в локальной области. Эти переменные также называются локальными переменными.

В JavaScript существует два вида локальной области видимости: область видимости функции и область видимости блока.

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

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

В приведенном ниже примере переменная hello находится в области sayHello:

function sayHello () {
  const hello = 'Привет, мир!'
  console.log(hello)
}

sayHello()  // => 'Привет, мир!'
console.log(hello)  // Получим ошибку, поскольку переменная hello не определена

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

Когда вы объявляете переменную с помощью const или let в фигурных скобках { } (например, это могут быть фигурные скобки оператора if или for), вы можете получить доступ к этой переменной только внутри этих фигурных скобок.

В приведенном ниже примере вы можете увидеть, что область действия hello ограничена фигурными скобками:

{
  const hello = 'Привет, мир!'
  console.log(hello)  // => 'Привет, мир!'
}

console.log(hello)  // Ошибка, т.к. переменная hello не объявлена

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

Хоистинг и область видимости функции

Функции, объявленные с помощью ключевого слова function, при чтении скрипта всегда поднимаются в верхнюю часть текущей области видимости (это поведение называется хоистинг). В примере ниже обе записи эквиваленты:

sayHello()
function sayHello () {
  console.log('Привет, мир!')
}
function sayHello () {
  console.log('Привет, мир!')
}
sayHello()

Функции, объявленные с помощью выражения, не поднимаются в верхнюю часть текущей области видимости.

sayHello()  // Ошибка, т.к. sayHello не определён
const sayHello = function () {
  console.log('Привет, мир!')
}

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

Функции не имеют доступа к областям действия друг друга

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

В примере ниже second не имеет доступа к firstFunctionVariable.

function first () {
  const firstFunctionVariable = 'Я в первой функции!'
}

function second () {
  first()
  console.log(firstFunctionVariable)  // Ошибка, firstFunctionVariable не определена
}

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

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

Однако внешняя функция не имеет доступа к переменным внутренней.

function outerFunction () {
  const outer = 'Я во внешней функции!'

  function innerFunction() {
    const inner = 'Я во внутренней функции!'
    console.log(outer)  // => 'Я во внешней функции!'
  }

  console.log(inner)  // Ошибка, т.к. inner не объявлена
}

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

как работает область видимости

Если у вас есть области видимости в пределах области видимости, визуализируйте, будто внутри комнаты ещё одна комната с таким же окном и т.д.

области видимости в пределах области видимости

Если вы понимаете всё, что касается областей видимости, то вы готовы к тому, чтобы понять, что такое замыкания.

Замыкания

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

function outerFunction () {
  const outer = 'Я - внешняя переменная!'

  function innerFunction() {
    console.log(outer)
  }

  return innerFunction
}

outerFunction()() // Выведет 'Я - внешняя переменная!'

Поскольку возвращается внутренняя функция, можно немного сократить код, написав оператор return при объявлении функции.

function outerFunction () {
  const outer = 'Я - внешняя переменная!'

  return function innerFunction() {
    console.log(outer)
  }
}

outerFunction()() // Выведет 'Я - внешняя переменная!'

Поскольку замыкания имеют доступ к переменным внешней функции, они обычно используются для двух целей:

  • Чтобы контролировать побочные эффекты
  • Чтобы создать частные переменные

Контроль побочных эффектов с помощью замыкания

Побочные эффекты возникают, когда вы делаете что-то помимо возврата значения из функции. Многие вещи могут быть побочными эффектами, например, запрос Ajax, тайм-аут или даже оператор console.log:

function (x) {
  console.log('console.log является побочным эффектом!')
}

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

Давайте рассмотрим это на примере, чтобы прояснить ситуацию.

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

function makeCake() {
  setTimeout(_ => console.log('Торт готов!'), 1000)
}

Как видите, у этой функции приготовления торта есть побочный эффект - тайм-аут.

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

function makeCake(flavor) {
  setTimeout(_ => console.log(`Торт со вкусом ${flavor} готов!`), 1000)
}

Когда вы запускаете функцию, обратите внимание, что торт готовится сразу через одну секунду.

makeCake('банан')
// Торт со вкусом банан готов!

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

Чтобы решить эту проблему, вы можете написать функцию prepareCake, которая хранит ваш вкус, а затем вернуть замыкание makeCake в prepareCake.

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

function prepareCake (flavor) {
  return function () {
    setTimeout(_ => console.log(`Торт со вкусом ${flavor} готов!`), 1000)
  }
}

const makeCakeLater = prepareCake('банан')

// И где-то позже в коде...
makeCakeLater()
// И получим 'Торт со вкусом банан готов!'

Этот пример демонстрирует, как замыкание используется для уменьшения побочных эффектов - вы создаете функцию, которая активирует внутреннее закрытие по вашей прихоти.

Частные переменные с замыканиями

Как вы уже знаете, переменные, созданные в функции, недоступны вне функции. Поскольку к ним нельзя получить доступ, они также называются частными переменными.

Однако иногда вам всё-таки нужно получить доступ к такой частной переменной. Сделать это можно с помощью замыканий.

function secret (secretCode) {
  return {
    saySecretCode () {
      console.log(secretCode)
    }
  }
}

const theSecret = secret('Секретный код')
theSecret.saySecretCode()
// Получим 'Секретный код'

saySecretCode в этом примере - единственная функция (замыкание), которая выводит secretCode вне исходной секретной функции, вследствие чего она также называется привилегированной функцией.

Отладка областей видимости с помощью DevTools

Инструменты разработчика DevTools в браузерах Chrome и Firefox могут значительно упростить отладку переменных, к которым вы можете получить доступ в текущей области видимости. Есть два способа использовать эту функциональность.

Первый способ - добавить в код ключевое слово debugger. Это принудит браузер приостановить выполнение скрипта для отладки.

Вот пример с нашим prepareCake:

function prepareCake (flavor) {
  debugger // добавим отладчик
  return function () {
    setTimeout(_ => console.log(`Торт со вкусом ${flavor} готов!`), 1000)
  }
}

const makeCakeLater = prepareCake('банан')

И теперь, если откроем инструменты DevTools и перейдем на вкладку «Sources» в Chrome (или вкладку «Debugger» в Firefox), то увидим доступные нам переменные.

доступные нам переменные

Также, можно переместить ключевое слово debugger в замыкание. Обратите внимание, как на этот раз изменяются переменные области видимости:

function prepareCake (flavor) {
  return function () {
    debugger // добавим отладчик
    setTimeout(_ => console.log(`Торт со вкусом ${flavor} готов!`), 1000)
  }
}

const makeCakeLater = prepareCake('банан')

можно переместить ключевое слово debugger в замыкание

Второй способ - добавить брейкпоинт непосредственно в отладчике, кликнув на номер строки.

добавить брейкпоинт непосредственно в отладчике

Заключение

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

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

Если вы определяете какую-либо внутреннюю функцию внутри другой функции, эта внутренняя функция называется замыканием. Она сохраняет доступ к переменным, созданным во внешней функции.