Ошибки, сложный материал, вопросы >
Нашли опечатку или неточность?

Выделите текст, нажмите ctrl + enter и отправьте его нам. В течение нескольких дней мы исправим ошибку или улучшим формулировку.

Что-то не получается или материал кажется сложным?

Загляните в раздел «Обсуждение»:

  • задайте вопрос нашим менторам. Вы быстрее справитесь с трудностями и прокачаете навык постановки правильных вопросов, что пригодится и в учёбе, и в работе программистом;
  • расскажите о своих впечатлениях. Если курс слишком сложный, подробный отзыв поможет нам сделать его лучше;
  • изучите вопросы других учеников и ответы на них. Это база знаний, которой можно и нужно пользоваться.
Об обучении на Хекслете

Возврат функций из функций

По своему опыту могу сказать, что возврат функций из функций вызывает наибольшие сложности у новичков. И дело даже не в том, что возврат сложен сам по себе, а в том, что поначалу очень сложно понять, зачем это может понадобиться. В реальной жизни эта техника используется часто, причем как в JS, так и во многих других языках. Функции, принимающие на вход функции, которые возвращают функции — обычное дело для любого кода на js.

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

Начнем погружение с уже пройденного материала:

const identity = (v) => v;
identity('wow'); // wow

const sum = identity((a, b) => a + b);
sum(1, 8); // 9

Функции — это такие же данные, как числа или строки, поэтому функции можно передавать в другие функции в виде аргументов, а также возвращать из функций. Мы даже можем определить функцию внутри другой функции и вернуть её наружу. И в этом нет ничего удивительного. Константы можно создавать где угодно.

const generateSumFinder = () => {
  const sum = (a, b) => a + b;     // создали функцию
  return sum;                      // и вернули её
};

const sum = generateSumFinder();   // sum теперь — функция, которую вернула функция generateSumFinder
sum(1, 5); // 6                 // sum складывает числа

Можно даже обойтись без промежуточного создания константы:

// вызвали функцию, которая возвращает функцию,
// и тут же вызвали возвращенную функцию

generateSumFinder()(1, 5); // 6
// ((a, b) => a + b)(1, 5)

Всегда, когда видите подобные вызовы f()()(), знайте: функции возвращаются!

Теперь посмотрим, как еще можно описать функцию generateSumFinder:

// предыдущий вариант для сравнения
// const generateSumFinder = () => {
//   const sum = (a, b) => a + b;
//   return sum;
// };

// новый вариант
const generateSumFinder = () => (a, b) => a + b;

Для понятности можно расставить скобки:

const generateSumFinder = () => ((a, b) => a + b);

Определение функции обладает правой ассоциативностью. Все, что находится справа от =>, считается телом функции. Количество вложений никак не ограничено. Вполне можно встретить и такие варианты:

const sum = (x) => (y) => (z) => x + y + z;

// расставим скобки для того чтобы увидеть как функции вложены друг в друга
// const sum = x => (y => (z => x + y + z));

sum(1)(3)(5); // 9

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

const inner1 = (z) => x + y + z;
const inner2 = (y) => inner1;
const sum = (x) => inner2;

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

sum(1)(3)(5); // 9

const sum1 = (x) => (y) => (z) => x + y + z;

// sum(1);
const sum2 = (y) => (z) => 1 + y + z; // inner2

// sum(1)(3)
const sum3 = (z) => 1 + 3 + z; // inner1

// sum(1)(3)(5)
const sum4 = 1 + 3 + 5; // 9

Как видно выше, sum1, sum2 и sum3 — это функции, а sum4 уже число, так как были вызваны все внутренние функции.

Давайте распишем все функции:

const sum = (x) => (y) => (z) => x + y + z;
// const sum = x => (y => (z => x + y + z));
  • Функция sum принимает x и возвращает функцию, которая
    • принимает y и возвращает функцию, которая
      • принимает z и возвращает сумму x + y + z

Попробуем развить идею функции callTwice из предыдущего урока. Напишем функцию generate, которая не применяет функцию сразу, а генерирует новую.

const generate = (f) => (arg) => f(f(arg));
// const generate = f => (arg => f(f(arg)));

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

Closure

Создадим функцию f1. Она будет той функцией, которую вернет generate если передать ей функцию Math.sqrt (она вычисляет квадратный корень числа).

Получается, f1 — это функция, которая принимает число и возвращает корень корня — Math.sqrt(Math.sqrt(x)):

const f1 = generate(Math.sqrt);
f1(16); // 2
// generate(Math.sqrt)(16);

Ещё пример: передадим в функцию generate новую функцию на ходу, без предварительного создания. Переданная функция возводит число в квадрат.

const f2 = generate(x => x ** 2);
f2(4); // 256
// generate(x => x ** 2)(4);

Теперь функция f2 возводит число в квадрат два раза: (42)2.

Функция generate имеет такое имя не просто так. Дело в том, что возврат функции порождает каждый раз новую функцию при каждом вызове, даже если тела этих функций совпадают:

const f1 = generate(x => x ** 2);
const f2 = generate(x => x ** 2);
console.log(f1 === f2); // => false

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

Замыкание

Работа практически всех описанных примеров базировалась на одном интересном свойстве, которое называется «замыкание». О нем говорилось в предыдущем курсе, но пришло время освежить память.

const generateDouble = (f) => (arg) => f(f(arg));
const f1 = generateDouble(Math.sqrt);

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

Но та функция, которую вернула generateDouble все еще использует аргумент. В обычных условиях он бы навсегда исчез, но тут он «запомнился» или «замкнулся» внутри возвращенной функции. Технически внутренняя функция, как и любая другая в JS, связана со своим лексическим окружением, которое не пропадает, даже если функция покидает это окружение.

Замыкание

Функция, которая была возвращена из generateDouble, называется замыканием. Замыкание — это функция, «запомнившая» часть окружения, где она была задана. Функция замыкает в себе идентификаторы (все, что мы определяем) из лексической области видимости.

В СИКП дается прекрасный пример на понимание замыканий. Представьте себе, что мы проектируем систему, в которой нужно запомнить пароль пользователя, а потом проверять его, когда пользователь будет заново заходить. Можно смоделировать функцию savePassword, которая принимает на вход пароль и возвращает предикат, то есть функцию, возвращающую true или false, для его проверки. Посмотрите, как это выглядит:

const secret = 'qwerty';
// Возвращается предикат.
const isCorrectPassword = savePassword(secret);

// Теперь можно проверять
console.log(isCorrectPassword('wrong password')); // => false
console.log(isCorrectPassword('qwerty')); // => true

А вот как выглядит код функции savePassword:

const savePassword = password => passwordForCheck => password === passwordForCheck;

Возврат функций в реальном мире (Debug)

Логгирование — неотъемлемая часть разработки. Для понимания того, что происходит внутри кода, используют специальные библиотеки, с помощью которых можно логгировать (выводить) информацию о проходящих внутри процессах, например в файл. Типичный лог веб-сервера, обрабатывающего HTTP запросы выглядит так:

[  DEBUG] [2015-11-19 19:02:30.836222] accept: HTTP/1.1 GET - / - 200, 4238
[   INFO] [2015-11-19 19:02:32.106331] config: server has reload its config in 200 ms
[WARNING] [2015-11-19 19:03:12.176262] accept: HTTP/1.1 GET - /info - 404, 829
[  ERROR] [2015-11-19 19:03:12.002127] accept: HTTP/1.1 GET - /info - 503, 829

В js самой популярной библиотекой для логгирования считается Debug. Вот как выглядит ее вывод:

Debug

Обратите внимание на левую часть каждой строки. Debug для каждой выводимой строчки использует так называемый неймспейс, некоторую строчку, которая указывает принадлежность выводимой строчки к определенной подсистеме или части кода. Он используется для фильтрации, когда логов становится много. Другими словами, можно указать "выводи сообщения только для http". А вот как это работает:

import debug from 'debug';

const logHttp = debug('http');
const logHandler = debug('handler');

logHttp('hello!');
logHttp('i am from http');

logHandler('hello from handler!');
logHandler('i am from handler');

Что приведет к такому выводу:

http hello! +0ms
http i am from http +2ms
handler hello from handler! +0ms
handler i am from handler +1ms

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


<span class="translation_missing" title="translation missing: ru.web.courses.lessons.mentors.mentor_avatars">Mentor Avatars</span>

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят менторы из команды Хекслета или другие студенты.

Для полного доступа к курсу нужна профессиональная подписка

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

Получить доступ
115
курсов
892
упражнения
2241
час теории
3196
тестов

Зарегистрироваться

или войти в аккаунт

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

  • 115 курсов, 2000+ часов теории
  • 800 практических заданий в браузере
  • 250 000 студентов

Нажимая кнопку «Зарегистрироваться», вы даёте своё согласие на обработку персональных данных в соответствии с «Политикой конфиденциальности» и соглашаетесь с «Условиями оказания услуг». Защита от спама reCAPTCHA «Конфиденциальность» и «Условия использования».

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Rambler
Логотип компании Bookmate
Логотип компании Botmother

Есть вопрос или хотите участвовать в обсуждении?

Зарегистрируйтесь или войдите в свой аккаунт

Нажимая кнопку «Зарегистрироваться», вы даёте своё согласие на обработку персональных данных в соответствии с «Политикой конфиденциальности» и соглашаетесь с «Условиями оказания услуг». Защита от спама reCAPTCHA «Конфиденциальность» и «Условия использования».