По своему опыту могу сказать, что возврат функций из функций вызывает наибольшие сложности у новичков. И дело даже не в том, что возврат сложен сам по себе, а в том, что поначалу очень сложно понять, зачем это может понадобиться. В реальной жизни эта техника используется часто, причем как в JavaScript, так и во многих других языках. Функции, принимающие на вход функции, которые возвращают функции — обычное дело для любого кода на JavaScript.
Для закрепления материала пройдитесь по нему два раза. Первый раз просто бегло прочитайте, второй раз изучите внимательно, проверяя каждую строчку кода.
Начнем погружение с уже пройденного материала:
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
принимает функцию в качестве аргумента и возвращает новую функцию. Внутри новой функции переданная изначально функция вызывается два раза:
Создадим функцию 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
, все еще использует аргумент. В обычных условиях он бы навсегда исчез, но тут он «запомнился» или «замкнулся» внутри возвращенной функции. Технически внутренняя функция, как и любая другая в JavaScript, связана со своим лексическим окружением, которое не пропадает, даже если функция покидает это окружение.
Функция, которая была возвращена из 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
В JavaScript самой популярной библиотекой для логгирования считается 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
— это функция, которая принимает на вход неймспейс в виде строки и возвращает другую функцию, которая уже используется для логгирования.
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.