JavaScript | Функция возвращает Функцию

JavaScript | Функция возвращает Функцию

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

Мы поэтапно дойдём до понимания сути вопроса «ЗАЧЕМ ЭТО НУЖНО?»

Но сперва вы должны знать, что функции могут иметь два состояния:

  1. Функция может быть ОБЪЯВЛЕНА
  2. Функция может быть ВЫЗВАНА

ОБЪЯВЛЕННАЯ функция по сути хранит инструкцию о действиях, которые нужно будет совершить в момент её вызова. ВЫЗВАННАЯ функция находит инструкцию с нужными действиями и выполняет их строго по списку.

Заметьте, что у Строк или Чисел очень сложно представить какое-либо иное состояние. Как они выглядят, тем они и являются. Число 10 — это 10, а строка «Привет мир!» — это строка «Привет мир!». Строка или Число это и есть итоговые данные, которые можно получить при прямом обращении к ним. С такими примитивами не нужно ничего вычислять.

Т. к. функции имеют разные состояния, то они также могут быть возвращены в разных состояниях:

  1. Функция может быть возвращена в качестве ОБЪЯВЛЕННОЙ (нам вернётся инструкция)
  2. Функция может быть возвращена в качестве ВЫЗВАННОЙ (нам вернётся результат выполненной инструкции)

 

Видеоролик

 

1. Самый простой пример возврата функций в двух состояниях

1.1 Функция возвращена в качестве ОБЪЯВЛЕННОЙ

Как это выглядит в простом примере?

function R(){
   return function G(){}
}

В этом примере функция R при вызове возвращает вложенную в неё ОБЪЯВЛЕННУЮ функцию G. Функция G НЕ ВЫЗВАНА, она отдана в качестве инструкции. Давайте посмотрим на скриншот из браузера:

Функция R вернула после вызова функцию G
Функция R вернула после вызова функцию G

В таком виде ВОЗВРАЩЁННАЯ ОБЪЯВЛЕННАЯ функция G не имеет абсолютно никакого смысла т. к. она ничего не делает. Но что мы поняли из этого примера?

Мы поняли то, что функция может возвращать не только результаты ВЫЗОВА других функций, но и САМИ ФУНКЦИИ БЕЗ ВЫЗОВА.

 

1.2 Функция возвращена в качестве ВЫЗВАННОЙ

function R(){
   return function G(){}()
}

Обратите внимание! В этих двух примерах отличается только пара круглых скобок () после объявления функции G. То есть во втором примере мы объявили функцию и тут же вызвали её. Что мы получим, когда вызовем функцию R ?

На этот раз мы получим undefined

Почему?

Потому что оператор return вернёт нам НЕ инструкцию функции G, а результат её вызова.

Функция R вернула после вызова результат вызова функции G - не инструкцию
Функция R вернула после вызова результат вызова функции G — не инструкцию

 

2. Полезное применение — Самый простой счётчик — Код шаблона

Чтобы до конца понять смысл возврата функций в качестве инструкций, предлагаю написать свой собственный счётчик. Его стартовое значение будет равно нулю — 0. Каждый вызов функции-счётчика будет увеличивать его значение на единицу. Мы сделаем такой универсальный счётчик, чтобы иметь возможность вешать его на любые процессы в программе.

Предлагаю назвать нашу функцию counter. Выглядеть она будет так:

function counter() {
   let x = 0;
   return function generator() {
      x = x + 1;
      return x;
   };
}

Давайте внимательно посмотрим что в ней происходит.

Первое выражение:

let x = 0;

Это выражение поможет нам зафиксировать в функции counter стартовое значение счётчика равным нулю. Переменная x объявлена внутри тела функции counter — это значит, что переменная x не видна вне тела функции counter. То есть к переменной x нельзя обратиться из другой части программы напрямую. К переменной x можно обратиться только изнутри функции counter и всех её вложенных функциях (если таковые будут, а они будут).

Второе выражение:

return function generator() {
   x = x + 1;
   return x;
}

Оно говорит нам о том, что результатом вызова функции counter будет являться возврат ОБЪЯВЛЕННОЙ функции generator. (Не ВЫЗОВ, А ОБЪЯВЛЕНИЕ). То есть инструкция.

Внутри тела функции generator мы ОБРАЩАЕМСЯ к какому-то существующему x (икс) и у этого x мы меняем значение на единицу в положительную сторону. И в конце возвращаем сам x.

 

3. Как пользоваться нашим шаблоном счётчика?

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

let s1 = counter()

Переменной s1 мы присваиваем результат вызова функции counter. Что мы имеем в s1 ?

Создали новый счётчик s1
Создали новый счётчик s1

Если сейчас посмотреть на набор инструкций, которые хранит s1, то мы почему то не увидим объявление переменной x (икс) с присваиванием нулевого значения. Куда оно делось? Исчезло? Нет.

В тот момент, когда мы присвоили переменной s1 вызов функции counter, мы ЗАМКНУЛИ переменную x (икс) со значением НОЛЬ внутри тела функции counter. С этого момента в переменной s1 начала жить НОВАЯ функция counter (её клон), которая навсегда заняла для переменной x (икс) значение в оперативной памяти устройства, на котором производится вызов этой НОВОЙ функции.

Достучаться до этого x (икс) извне невозможно т. к. нет прямого доступа к переменной x (икс). ЗАМЫКАНИЕ не позволяет этого сделать. Теперь на переменную x (икс) может влиять только функция generator, которая в настоящий момент является s1.

При прямом обращении к инструкции s1 мы будем видеть только ОБЪЯВЛЕННУЮ функцию generator. Вызов s1 будет вызывать generator.

Давайте же сделаем наконец наш первый вызов функции s1

s1()

Скриншот:

Первый вызов счётчика s1
Первый вызов счётчика s1

Сделаем ещё 3 вызова:

s1()
s1()
s1()

Скриншот:

Четыре вызова счётчика s1
Четыре вызова счётчика s1

Четвёртый вызов функции s1 вернул нам значение 4. Четвёртый вызов функции s1 навсегда изменил значение ЗАМКНУТОЙ переменной x (икс) в клоне функции counter.

 

4. Зачем мы это замудрили?

Все эти манипуляции были созданы для того, чтобы иметь возможность создавать любое количество НЕЗАВИСИМЫХ друг от друга счётчиков, под любые задачи.

У нас уже есть счётчик s1. Последним значением он вернул 4. Давайте теперь создадим новый счётчик и назовём его ddd.

let ddd = counter()

Запустим счётчик ddd два раза.

ddd()
ddd()

А теперь запустим s1.

s1()

Смотрим историю вызовов:

Создали и запустили новый счётчик ddd
Создали и запустили новый счётчик ddd

Второй вызов ddd вернул нам значение 2. Следующий за ним вызов s1 вернул нам значение 5. Это значит, что в оперативной памяти появилась новая замкнутая переменная x (икс), которая живёт во втором клоне оригинальной функции counter.(в ddd)

 

5. Итог

Хочу подвести общий итог из всего перечисленного. В основном функция возвращает функцию в тех случаях, когда мы хотим написать независимую логику работы для вызовов внутренней функции, которая знает о существовании каких-то замкнутых переменных за её пределами, но не выше самой функции-шаблона. Замыкание срабатывает в момент объявления

Такой подход в официальной семантике программирования называется «Шаблоны проектирования» (Design patterns). В общей практике проектирования систем их существует огромное количество. Но это уже другая история.