В этой статье мы рассмотрим две важные концепции Javascript: область видимости и замыкание.

Объем функций

Начнем с самого простого прицела. Если вы когда-нибудь писали на Javascript, вы, вероятно, сталкивались с таким поведением.

Взгляните на этот фрагмент кода:

function addOne(a) {
   const b = a + 1;
}
console.log(b); // ReferenceError: b is not defined

Мы определяем b в функции addOne и пытаемся получить к ней доступ позже. Это дает нам ReferenceError — переменные, объявленные в функции, автоматически невидимы вне ее. b называется областью действия.

Это естественно и ожидаемо — нам нравится определять новые временные переменные в функциях, не вмешиваясь во внешний код.

Глобальный охват

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

В нашем предыдущем примере функция addOne была определена в глобальной области видимости.

Что мы склонны забывать, так это то, что глобальная область видимости часто содержит гораздо больше функций. Например, когда мы вызываем console.log, объект console существует в глобальной области видимости — действительно, его можно вызывать из любого места в нашем коде.

Различие, которое мы рассмотрели до сих пор, было всем, что было возможно до ES6, и оно кажется довольно естественным — в дополнение к тому, что оно похоже на то, как работают другие языки.

Область блока

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

Блок — это просто группа из нуля или более операторов. Чтобы разграничить блок, мы используем фигурные скобки:

// Block beginning
 {
   const a = 2; // Statement 1;
   let b = a + 2; // Statement 2;
 }
 // Block end

Блоки используются повсюду в JS — вы, вероятно, уже использовали их с условиями if/else, циклами, операторами switch…

Объявления с блочной областью действия

Когда вы определяете переменную в блоке с помощью let, const или class, она полностью невидима вне его:

if (1 + 1 === 2) {
   // This block will run only if 1 + 1 === 2 is true (it should be)
   const me = "Math expert";
}
console.log(me); // ReferenceError: me is not defined :(

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

const friends = ["Elise", "Joseph"];

{
   const secretFriend = "Darkus";
   friends.push(secretFriend);
}

console.log(friends); // [ 'Elise', 'Joseph', 'Darkus' ]
console.log(secretFriend); // ReferenceError: secretFriend is not defined

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

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

for (let i = 0; i < 5; i++) {
   console.log(i);
   if (i ** 2 >= 3) {
     break;
   }
}
console.log(i); // ReferenceError: i is not defined

Разве область действия блока не совпадает с областью действия функции?

Область видимости блока и функции очень похожа — у нас может возникнуть соблазн сказать, что тело функции создает блок.

Проблема в том, посмотрите, как var ведет себя в блоке:

if (Math.random() >= 0) {
   var wonAmount = 10_000; // Cool way to use the 10000 literal btw
   const lostAmount = 100;
}
console.log(wonAmount); // 10000
console.log(lostAmount); // ReferenceError: lostAmount is not defined

Несмотря на то, что мы создали блок для нашего оператора if, переменная wonAmount была доступна за его пределами.

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

Но объявленные переменные var просто игнорируют область действия блока: они относятся к более высокой области видимости (в данном случае глобальной). Короче говоря, область видимости блока полезна только для const и let. Объявленные переменные var в основном игнорируют это.

Небольшое примечание №1. Временная мертвая зона

Теперь, когда мы знаем, что переменные, объявленные var, ведут себя иначе, чем переменные, объявленные с помощью let/const, давайте посмотрим, что происходит в блоке, прежде чем мы определим наши переменные:

function hideFromOutside() {
   console.log(old); // (1) undefined
   console.log(young); // (2) ReferenceError
 
   if (1 + 2 === 3) {
     console.log(old); // (3) undefined
 
     // Temporal Dead Zone:
     // (4) ReferenceError: Cannot access 'young' before initialization
     console.log(young);
 
     var old = 3;
     let young = 2;
 
     console.log(old); // (5) 3
     console.log(young); // (6) 2
   }
 }
 
 console.log(old); // (7) ReferenceError
 console.log(young); // (8) ReferenceError
 
 hideFromOutside();

В блоке if функции hideFromOutside мы объявляем две переменные: old с var и young с пусть.

Как мы видели, old объявляется в верхней части своей области видимости, области видимости функции (поскольку она не видит область действия блока). В (1) и (3) он еще не определен, но не выдает ReferenceError. Как только мы его определим, мы сможем получить доступ к его значению в (5). Вне области действия функции (в (7)) она невидима.

То же самое ожидается и для young: let объявляет его в области видимости if, поэтому он невидим в (2) и (8), что приводит к ошибке ReferenceError. Как только мы его определим, мы сможем получить доступ к его значению в (6).

Но что происходит в (4) в области блока, но до его инициализации? молодой находится в так называемой Временной Мертвой Зоне (TDZ) (это не шутка), куда к нему пока нет доступа. Мы получаем другой тип ReferenceError: Не удается получить доступ к ‘молодому’ перед инициализацией.

Примечание № 2. Переопределение переменных

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

Более короткий ответ: «Не делай этого, ты запутаешься».

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

const name = "Jonah"; // Global name
 
 function logName() {
   // No function-scoped name, look in the global scope
   console.log(name); // Jonah
   if (true) {
     console.log(name); // ReferenceError: Cannot access 'name' before initialization
     const name = "Alice"; // Block-scoped name
     console.log(name); // Alice
   }
   console.log(name); // Jonah
 }
 
 console.log(name); // Jonah

В блоке if функции logName мы переопределяем переменную name. Вне блока его значение наследуется от глобальной области видимости, где установлено значение «Иона». В блоке переменной присваивается значение «Алиса». До этой инициализации находится в ТДЗ (см. предыдущий раздел).

Небольшое примечание № 3. Область видимости в модулях ES

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

Надеюсь, это поможет вам понять, как в современном Javascript определяются области видимости переменных.

Закрытие

Теперь, когда мы понимаем масштаб, давайте попробуем немного оживить ситуацию!

Взгляните на следующий пример:

function createGreeter(greeting) {
   return function greet(name) {
     console.log(`${greeting}, ${name}!`);
   };
 }
 
 const formalGreeter = createGreeter("Hello");
 const oldSchoolFriendlyGreeter = createGreeter("Wasssup");
 
 formalGreeter("world"); // Hello, world!
 oldSchoolFriendlyGreeter("folks"); // Wasssup, folks!

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

Почему это работает? Всякий раз, когда мы создаем функцию в Javascript, она создает среду со всем, что может видеть в данный момент. Здесь функция greet имеет доступ к аргументу greeting, поэтому мы можем использовать его в теле.

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

Использование замыканий для каррирования функций

Если вам нравится функциональное программирование, вы, вероятно, знакомы с концепцией каррирования.

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

Предположим, у нас есть простая функция загрузки, которая принимает URL-адрес и формат, ПОЛУЧАЕТ JSON из URL-адреса и выводит его в выбранном формате (журнал, текст, csv, excel…).

Возможно, мы реализовали это следующим образом:

const download = async (url, format) => {
   const data = await get(url);
   switch (format) {
     case "log":
       console.log(data);
       break;
     case "txt":
       writeToFile(data);
       break;
     default:
       throw new Error(`Unknown format ${format}`);
   }
 };
 
 await download("https://jsonplaceholder.typicode.com/todos", "log"); // logs an object

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

Кроме того, если мы хотим сохранить одни и те же данные в текстовом файле и файле Excel, в настоящее время нам нужно получить их дважды.

Чтобы решить эти проблемы, мы можем сначала создать замыкание:

const download = async (url, format) => {
   const data = await get(url);
   const exportData = () => {
     switch (format) {
       case "log":
         console.log(data);
         break;
       case "txt":
         writeToFile(data);
         break;
       default:
         throw new Error(`Unknown format ${format}`);
     }
   };
   exportData();
 };
 
 await download("https://jsonplaceholder.typicode.com/todos", "log"); // logs an object

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

Следующий шаг — переместить параметр format во внутреннюю функцию и вернуть его вместо вызова:

const download = async (url) => {
   const data = await get(url);
   return (format) => {
     switch (format) {
       case "log":
         console.log(data);
         break;
       case "txt":
         writeToFile(data);
         break;
       default:
         throw new Error(`Unknown format ${format}`);
     }
   };
 };

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

// Run this as a module to use the top-level await below
const todosExporter = await download(
   "https://jsonplaceholder.typicode.com/todos"
 );
 todosExporter("log"); // Logs an object
 todosExporter("csv"); // Fails with Error: Unknown format csv

Но почему это полезно?

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

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

const createCounter = function () {
   let guestCount = 0;
   return {
     addGuests: (newGuests) => (guestCount += newGuests),
     print: () => console.log(guestCount),
   };
 };
 
 const counter = createCounter();
 counter.print(); // 0
 counter.addGuests(5);
 counter.print(); // 5
 console.log(counter.guestCount); // undefined

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

Он работает так же, как и в предыдущем примере: внутренние функции имеют доступ к среде, в которой они были определены (окружающий контекст), а внешняя функция ограничивается guestCount. переменная, чтобы убедиться, что она невидима снаружи.

Можем ли мы использовать для этого закрытые поля?

Если вы недавно следили за изменениями JS, возможно, вы знаете, что теперь вы можете определять частные поля для объектов, используя синтаксис #property (произносится как свойство хэш).

Наш предыдущий пример можно было бы переписать так:

class Counter {
   #guests = 0;
   print() {
     console.log(this.#guests);
   }
   add(newGuests) {
     this.#guests += newGuests;
   }
 }
 
 const counter = new Counter();
 counter.print(); // 0
 counter.add(3);
 counter.print(); // 3
 console.log(counter.#guests); // SyntaxError

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

Заключение

Вот и все, теперь вы знаете, что такое область действия и замыкание в Javascript!