Коли ми кажемо функції "першого класу", ми кажемо, що вони такі як і всі...іншими словами - звичайний клас. Ми можемо розцінювати функції, як будь-який інший тип даних і в них немає нічого такого особливого: вони можуть зберігатися в масивах, передаватись в якості аргументів до інших функцій, їх можна призначити змінним і взагалі, зробити усе, що заманеться.
Усе наведене нижче - це основи JavaScript, але пройшовшись по коду на github можна зрозуміти, що багатьма такий підхід просто ігнорується. Чи розглянемо ми один вигаданий приклад? Чом би й ні:
const hi = name => `Hi ${name}`;
const greeting = name => hi(name);
Як ви вже могли зрозуміти, обгортка навколо hi
у функції greeting
просто непотрібна. Чому? Та тому, що функції у JavaScript є викликаємими. Коли після методу hi
вказані дужки ()
- це значить, що вкінці вона запуститься і поверне значення. А якщо дужок немає - метод просто поверне функцію, записану у змінну. Щоб переконатися, просто погляньте самі:
hi; // name => `Hi ${name}`
hi("jonas"); // "Hi jonas"
Оскільки greeting
лише викликає метод hi
з тим самим аргументом, ми можемо спростити наш код:
const greeting = hi;
greeting("times"); // "Hi times"
Інакше кажучи, hi
вже являється функцією, яка очікує єдиний аргумент, тож навіщо її обгортати іншою функцією, яка б просто викликала hi
з тим єдиним нещасним аргументом? Це просто безглуздо. Це всеодно, що посеред липня вдягнути найтеплішу парку, для того, щоб запікатися і потребувати морозивка для охолодження.
Не вистачить жодних слів та емоцій, щоб передати наскільки ж це погано обгортати одну функцію іншою, лише тільки задля відтермінування її виконання.
Впевненне розуміння цього є вкрай важливим для продовження нашої мандрівки, тож давайте поглянемо на ще кілька прикольних прикладів з бібліотек, що були викопані з недр npm-пакетів.
// неосвічений підхід
const getServerStuff = callback => ajaxCall(json => callback(json));
// просвітлений
const getServerStuff = ajaxCall;
Всесвіт просто переповнений подібними цьому реалізаціями ajax-запитів. Ось чому обидва приклади - це одне й те саме:
// ця лінія
ajaxCall(json => callback(json));
// рівнозначна цій лінії
ajaxCall(callback);
// тож getServerStuff можна переписати
const getServerStuff = callback => ajaxCall(callback);
// ...і це буде рівносильно ось цьому
const getServerStuff = ajaxCall; // <-- глянь мам, жодних дужок ()
І це, друзі, те, як воно має робитися. І давайте ще один разочок, для кращого розуміння, чому я такий наполегливий.
const BlogController = {
index(posts) { return Views.index(posts); },
show(post) { return Views.show(post); },
create(attrs) { return Db.create(attrs); },
update(post, attrs) { return Db.update(post, attrs); },
destroy(post) { return Db.destroy(post); },
};
Цей абсурдний контролер на 99% - пшик. Ми могли б його краще переписати отак:
const BlogController = {
index: Views.index,
show: Views.show,
create: Db.create,
update: Db.update,
destroy: Db.destroy,
};
... Ой-ой, здається воно все в купі тому, що воно нічогісінько не робить окрім як з'єднує разом наші в'юхи (view (eng) - показувати, те що відповідає за відображення, прим. перекл.) з нашою базою даних.
Ну що ж, давайте нарешті перейдемо до причин, чому ж варто цінувати функції першого класу.
Як ми побачили на прикладах з getServerStuff
та BlogController
, дуже легко додавати прошарки, які не додають жодної цінності, а лише збільшують кількість непотрібного коду, який потім доведеться підтримувати і в якому копирсатися.
В додачу до всього, якщо нам потрібно щось змінити у функції, яку ми обгорнули непотрібним контейнером - нам доведеться змінювати і сам цей контейнер.
httpGet('/post/2', json => renderPost(json));
Якщо нам захочеться змінити httpGet
, для відправки можливого err
, то нам доведеться змінювати і "клей".
// повертається до кожного виклику httpGet у додатку/програмі та явно передає `err`.
httpGet('/post/2', (json, err) => renderPost(json, err));
Якби ми написали цей код з використанням функції першого класу - нам би довелось вносити менше змін:
// `renderPost` виклиаканий у рамках `httpGet` з усіма потрібними йому аргументами
httpGet('/post/2', renderPost);
Окрім того, щоб видаляти непотрібні функції, ми маємо іменувати і посилатись на аргументи, які передаємо. Але з іменами теж можуть виникати проблеми, особливо за умови розростання та старішання кодової бази.
Мати кілька назв для однієї і тієї ж самої концепції - досить розповсюджена причина непорозуміннь у проектах. Ось, наприклад, дві функції, які роблять абсолютно одне й те саме, проте одна з них виглядає більш загальною і багаторазовою:
// конкретно для нашого блогу
const validArticles = articles =>
articles.filter(article => article !== null && article !== undefined),
// більш актуальна для майбутніх проектів
const compact = xs => xs.filter(x => x !== null && x !== undefined);
Використовуючи більш конкретизовані назви для функцій, ми прив'язуємо самі себе до якихось конкретних даних (в нашому випадку articles
). Таке трапляється і над цим треба працювати та вдосконалюватись.
Я маю відзначити, що як і з об'єктно-орієнтованим кодом, ви маєте пам'ятати, що this
може болюче вжалити. Якщо основні функції вживають this
і ми називаємо їх функціями першого класу, ми маємо бути дуже обережними.
const fs = require('fs');
// страшнувато
fs.readFile('freaky_friday.txt', Db.save);
// трохи менше
fs.readFile('freaky_friday.txt', Db.save.bind(Db));
Прив'язавши Db
до самого себе, ми надаємо йому вільний доступ до його сміттєвого коду з його ж прототипу. Я намагаюсь уникати використання this
як брудного памперса. В цьому немає жодної потреби при написанні функціонально коду. Однак, при взаємодії з іншими бібліотеками, вам таки доведеться прийняти божевільність оточуючого нас Світу.
Хтось може посперечатися, мовляв this
важливе для оптимізації швидкості. Будь ласка, якщо ви один з тих фанатів мікро-оптимізацій - просто закрийте цю книгу. Навіть якщо ви не можете отримати свої гроші назад - спробуйте обміряйте її на щось більш потрібне для вас.
І тепер, ми готові перейти до наступного кроку.