Данная публикация представляет собой перевод материала «JavaScript Scope and Closures» под авторством Zell Liew, размещенного здесь.

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


Начнем с областей видимости


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


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



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


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


Примечание: это верно только для JavaScript в веб браузерах. В Node.js глобальные переменные объявляются иначе, но мы не будем касаться Node.js в этой статье.


const globalVariable = 'some value';

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


const hello = 'Hello CSS-Tricks Reader!';

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

console.log(hello); // 'Hello CSS-Tricks Reader!'
sayHello(); // 'Hello CSS-Tricks Reader!'

Хотя можно объявлять переменные в глобальной области видимости, но не рекомендуется это делать. Всё из-за того, что существует вероятность пересечения имен, когда двум или более переменным присваивают одинаковое имя. Если переменные объявляются через const или let, то каждый раз, когда будет происходить пересечение имён, будет показываться сообщение об ошибке. Такое поведение нежелательно.


// Не делайте так!
let thing = 'something';
let thing = 'something else'; // Ошибка, thing уже была объявлена

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


// Не делайте так!
var thing = 'something';
var thing = 'something else'; // возможно где-то в коде у переменной совершенно другое значение
console.log(thing); // 'something else'

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


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


Переменные, которые используются только в определенной части кода, считаются помещенными в локальную область видимости. Такие переменные называются локальными.


В JavaScript выделяют два типа локальных областей видимости:


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

Сначала рассмотрим область видимости функции


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


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


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


function sayHello () {
  const hello = 'Hello CSS-Tricks Reader!';
  console.log(hello);
}

sayHello(); // 'Hello CSS-Tricks Reader!'
console.log(hello); // Ошибка, hello не определена

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


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


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


{
  const hello = 'Hello CSS-Tricks Reader!';
  console.log(hello); // 'Hello CSS-Tricks Reader!'
}

console.log(hello); // Ошибка, hello не определена

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


Подъем функции в области видимости


Функции, объявленные как «function declaration» (прим. перев.: функция вида function имя(параметры) {...}), всегда поднимаются наверх в текущей области видимости. Так, два примера ниже эквивалентны:


// Тоже самое, что пример ниже
sayHello();
function sayHello () {
  console.log('Hello CSS-Tricks Reader!');
}

// Тоже самое, что пример выше
function sayHello () {
  console.log('Hello CSS-Tricks Reader!');
}
sayHello();

Если же функция объявляется как «function expression» (функциональное выражение) (прим. перев.: функция вида var f = function (параметры) {...}), то такая функция не поднимается в текущей области видимости.


sayHello(); // Ошибка, sayHello не определена
const sayHello = function () {
  console.log(aFunction);
}

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


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


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


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


function first () {
  const firstFunctionVariable = `I'm part of first`;
}

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

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


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


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


function outerFunction () {
  const outer = `I'm the outer function!`;

  function innerFunction() {
    const inner = `I'm the inner function!`;
    console.log(outer); // I'm the outer function!
  }

  console.log(inner); // Ошибка, inner не определена
}

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



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



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


Замыкания


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


function outerFunction () {
  const outer = `I see the outer variable!`;

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

  return innerFunction;
}

outerFunction()(); // I see the outer variable!

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


function outerFunction () {
  const outer = `I see the outer variable!`;

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

outerFunction()(); // I see the outer variable!

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


  1. контроля побочных эффектов;
  2. создания приватных переменных.

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


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


function (x) {
  console.log('A console.log is a side effect!');
}

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


Для пояснения рассмотрим пример


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


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


function makeCake() {
  setTimeout(_ => console.log(`Made a cake`), 1000);
}

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


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


function makeCake(flavor) {
  setTimeout(_ => console.log(`Made a ${flavor} cake!`), 1000);
}

После вызова функции торт будет испечён ровно через секунду.


makeCake('banana'); // Made a banana cake!

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


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


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


function prepareCake (flavor) {
  return function () {
    setTimeout(_ => console.log(`Made a ${flavor} cake!`), 1000);
  }
}

const makeCakeLater = prepareCake('banana');

// Позже в вашем коде...
makeCakeLater(); // Made a banana cake!

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


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


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


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


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

const theSecret = secret('CSS Tricks is amazing');
theSecret.saySecretCode(); // 'CSS Tricks is amazing'

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


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


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


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


Ниже пример с prepareCake:


function prepareCake (flavor) {
  // Добавляем debugger
  debugger
  return function () {
    setTimeout(_ => console.log(`Made a ${flavor} cake!`), 1000);
  }
}

const makeCakeLater = prepareCake('banana');

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



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


function prepareCake (flavor) {
  return function () {
    // Добавляем debugger
    debugger
    setTimeout(_ => console.log(`Made a ${flavor} cake!`), 1000);
  }
}

const makeCakeLater = prepareCake('banana');


Второй способ: добавлять брейкпоинт напрямую в код во вкладке Sources (или Debugger) путем клика на номер строки.



Выводы:


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

Комментарии (11)


  1. ya-est
    24.09.2017 12:01
    -1

    Есть ошибка. Если вызвать функцию определенную с помощью function expression до ее определения, будет TypeError: sayHello is not a function


    1. NarekPK Автор
      24.09.2017 23:35

      Ошибки нет, т.к. функция sayHello объявлена через const, а не через var.


      1. ya-est
        25.09.2017 10:22

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


        1. NarekPK Автор
          25.09.2017 21:38

          В описании к примеру функция вида «function expression» приведена с объявлением через var для наиболее общего вида. Если бы статья не была переводом, то в примере исправил бы const на var, но т.к. это не так, то оставим, как есть.


      1. ya-est
        25.09.2017 10:25

        Ну да и ошибка там reference error будет, а не то, что неопределено значение


        1. NarekPK Автор
          25.09.2017 21:49

          — Если запустить этот кусок кода в консоли браузера Chrome, то будет выведено: «sayHello is not defined», а это означает, что функция sayHello не определена (не объявлена), что и написано в примере.
          — В целом не ставилась задача точно отобразить, какая ошибка будет при выполнении кода. Важно было показать, что функция вида «function expression» не будет выполнена, если будет вызвана до своего объявления.


  1. Juul
    24.09.2017 23:35

    До меня дошло, спасибо!)


    1. NarekPK Автор
      24.09.2017 23:41

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


  1. X-3mal
    25.09.2017 21:58

    По мелочи:
    setTimeout(_ => console.log(`Made a ${flavor} cake!`, 1000)); — опечатка с закрывающимися скобками.


    1. NarekPK Автор
      25.09.2017 22:00

      Исправил, спасибо за подсказку.


  1. japson
    25.09.2017 22:04

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

    Модули требуют две ключевых характеристики: 1) внешнюю функцию-обертку, которую будут вызывать, чтобы создать закрытую область видимости 2) возвращаемое значение функции-обертки должно включать в себя ссылку на не менее чем одну внутреннюю функцию, у которой потом будет замыкание на внутреннюю область видимости обертки.
    Kyle Simpson: Вы не знаете JS