Области видимости и замыкания важны в 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!
Благодаря замыканиям появляется доступ к внешней функции, поэтому они обычно используются для двух целей:
- контроля побочных эффектов;
- создания приватных переменных.
Контроль побочных эффектов с помощью замыканий
Побочные эффекты появляются, когда производятся какие-то дополнительные действия помимо возврата значения после вызова функции. Множество вещей может быть побочным эффектом, например, 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) путем клика на номер строки.

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