Когда мы говорим о функциях «первого класса», то имеем в виду, что они того же класса, как и все остальные значения. С функциями можно обращаться так же, как и с любым другим типом данных, и в них нет ничего особенного: их можно хранить в массивах, передавать в другие функции как аргументы, использовать в качестве значений переменных, — всё, что душе угодно.
Это азы 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;
Мир JavaScript засорён точно такими же фрагментами кода. Вот почему оба примера выше — одно и то же:
// эта строка
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,
};
...или просто выкинуть его полностью, ведь он не делает ничего кроме объединения Views
и Db
.
Хорошо, давайте обсудим причины, по которым стоит предпочитать функции первого класса. Как мы уже видели в примерах с getServerStuff
и BlogController
, очень легко начать добавлять уровни абстракции, которые не содержат в себе никакой ценности, и только наращивают объем избыточного кода, который придется поддерживать или пытаться что-нибудь в нем искать.
К тому же, если сигнатура вложенной функции поменяется, нам придётся также менять и внешнюю функцию.
httpGet('/post/2', json => renderPost(json));
Если вдруг httpGet
должна начать использовать новый аргумент err
, то необходимо отредактировать и «функцию-склейку»:
// найти каждый вызов httpGet в приложении и добавить err
httpGet('/post/2', (json, err) => renderPost(json, err));
Если бы мы использовали renderPost
как функцию первого класса, переделывать пришлось бы гораздо меньше:
// rednerPost вызывается внутри 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
, а мы вызовем её как функцию первого класса, то почувствуем на себе весь гнев потери контекста (здесь имеется в виду контекст в понимании JavaScript; при вызове функции через точку после имени объекта происходит указание на контекст этого объекта; при передаче функции в качестве значения, вне зависимости от того, является ли она собственным свойством объекта или содержится в цепочке прототипов, привязка контекста не происходит, и это, скорее всего, приведет к ошибке — прим. пер.).
const fs = require('fs');
// страшновато
fs.readFile('freaky_friday.txt', Db.save);
// не так страшно
fs.readFile('freaky_friday.txt', Db.save.bind(Db));
С помощью bind
мы жестко привязываем контекст выполнения save
к объекту Db
и даём функции возможность использовать хлам из его прототипа. Я стараюсь избегать this
как грязных подгузников, да и в нём нет никакой необходимости при написании функционального кода. Однако, используя внешние библиотеки, не забывайте про безумный мир вокруг нас.
Некоторые могут поспорить, утверждая, что использование this
необходимо с точки зрения производительности. Если вы из микро-оптимизаторов, пожалуйста, закройте эту книгу. Если вам не удастся вернуть за неё деньги, то, я надеюсь, сможете её обменять на что-нибудь более замороченное.
Теперь мы готовы продолжать.