Данная публикация представляет собой перевод материала «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) путем клика на номер строки.



Выводы:


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