З власного досвіду можу сказати, що повернення функцій з функцій викликає найбільші труднощі у новачків. І справа навіть не в тому, що повернення складний сам по собі, а в тому, що спочатку дуже складно зрозуміти, навіщо це може знадобитися. У реальному житті ця техніка використовується часто, причому як в 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
- приймає z і повертає функцію, яка
- приймає y і повертає функцію, яка
Спробуємо розвинути ідею функції 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 все ще використовує аргумент. У звичайних умовах він би назавжди зник, але тут він «запам'ятався» або «замкнулося» всередині повернутої функції. Технічно внутрішня функція, як і будь-яка інша в 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 для кожної виведеної рядки використовує так званий неймспейс, деяку рядок, яка вказує приналежність виведеної рядки до певної підсистемі або частини коду. Він використовується для фільтрації, коли логів стає багато. Іншими словами, можна вказати "виводь повідомлення тільки для 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 - це функція, яка приймає на вхід неймспейс у вигляді рядка і повертає іншу функцію, яка вже використовується для логгірованія.