Когда мы говорим о функциях «первого класса» мы имеем в виду то, что они такие же как и все остальные...[^учитель, поясните!]. С такими функциями можно обращаться так же, как и с любым другим типом данных, в функциях первого класса нет ничего особенного: их можно хранить в массивах, передавать в функции в качестве аргумента, присваивать переменным — всё, что душе угодно.
Пример ниже — это азы JavaScript. Тем не менее, поискав по коду на github, легко удостовериться, что большинство старательно игнорирует подобный подход. Соскучились по надуманным примерам? Пожалуйста:
var hi = function(name){
return "Hi " + name;
};
var greeting = function(name) {
return hi(name);
};
Здесь совершенно не нужно оборачивать hi
в функцию greeting
. Почему? Потому что в JavaScript функции являются вызываемыми. Если написать hi
и добавить ()
на конце, то функция будет вызвана и вернёт какое-то значение. Если не дописывать скобки на конце, то будет возвращена сама функция, сохранённая в переменную. Убедимся в этом:
hi;
// function(name){
// return "Hi " + name
// }
hi("jonas");
// "Hi jonas"
Поскольку greeting
не делает ничего, кроме вызова hi
с тем же самым аргументом, то можно написать проще:
var greeting = hi;
greeting("times");
// "Hi times"
Другими словами, hi
— уже функция с одним аргументом, зачем же оборачивать её в ещё одну функцию, которая будет вызывать ту же hi
с тем же аргументом? Бессмыслица какая-то. Это как зимой надеть шубу поверх тёплой куртки, при том, что вам и так тепло.
Оборачивать функцию другой функцией просто для того, чтобы отложить её вызов — это не только слишком многословно, но ещё и считается плохой практикой (чуть ниже вы поймёте, почему, но намекну: речь идёт о поддержке кода).
Очень важно, чтобы вы поняли почему это так, прежде чем мы продолжим, поэтому позвольте мне привести несколько забавных примеров, которые я нашёл в существующих npm-пакетах:
// невежа
var getServerStuff = function(callback){
return ajaxCall(function(json){
return callback(json);
});
};
// прозрел!
var getServerStuff = ajaxCall;
Мир JavaScript засорён подобным кодом. Вот почему оба примера выше — одно и то же:
// эта строка
return ajaxCall(function(json){
return callback(json);
});
// равносильна этой
return ajaxCall(callback);
// перепишем getServerStuff
var getServerStuff = function(callback){
return ajaxCall(callback);
};
// что эквивалентно следующему
var getServerStuff = ajaxCall; // <-- смотри, мам, нет ()
Вот так это делается. Ещё один пример, затем я объясню почему же я так настаиваю.
var BlogController = (function() {
var index = function(posts) {
return Views.index(posts);
};
var show = function(post) {
return Views.show(post);
};
var create = function(attrs) {
return Db.create(attrs);
};
var update = function(post, attrs) {
return Db.update(post, attrs);
};
var destroy = function(post) {
return Db.destroy(post);
};
return {
index: index, show: show, create: create, update: update, destroy: destroy
};
})();
Код этого контроллера нелеп на 99%, мы можем легко переписать его:
var BlogController = {
index: Views.index,
show: Views.show,
create: Db.create,
update: Db.update,
destroy: Db.destroy
};
...или просто выкинуть его полностью, ведь он не делает ничего кроме объединения наших Views
и Db
.
Хорошо, давайте обсудим причины использования именно функций первого класса. Как мы уже видели в примерах с getServerStuff
и BlogController
, добавить бесполезный уровень абстракции легко, но зачем? Это только увеличивает количество кода, который необходимо поддерживать и читать.
К тому же, если сигнатура внутренней функции поменяется, нам придётся также менять и внешнюю функцию.
httpGet('/post/2', function(json){
return renderPost(json);
});
Если вдруг httpGet
станет принимать новый аргумент err
, то необходимо отредактировать и «функцию-склейку»:
// найти каждый вызов httpGet в приложении и добавить err
httpGet('/post/2', function(json, err){
return renderPost(json, err);
});
Изменений потребовалось куда меньше, если бы мы с самого начала воспользовались функцией первого класса:
// rednerPost вызывается внутри httpGet с любым количеством аргументов
httpGet('/post/2', renderPost);
Помимо определения лишних функций, нам также приходится придумывать названия аргументам, что само по себе не всегда так просто, особенно с ростом приложения.
Одной из частых проблем в проектах является как раз использование разных имён для одних и тех же понятий. Также стоит упомянуть момент с обобщением имён. Ниже обе функции делают одно и тоже, но последняя кажется более общей и, следовательно, более переиспользуемой:
// специфична для нашего конкретного приложения-блога
var validArticles = function(articles) {
return articles.filter(function(article){
return article !== null && article !== undefined;
});
};
// более общая, легко переиспользовать в другом проекте
var compact = function(xs) {
return xs.filter(function(x) {
return x !== null && x !== undefined;
});
};
Когда мы даём имена функциям, мы привязываем их к данным (в данном случае к articles
). Это происходит чаще, чем кажется, и является источником изобретения многих «велосипедов».
Я должен также упомянуть, что как и при объектно-ориентированном подходе, нужно опасаться того, что this
подкрадётся сзади и укусит вас за пятку. Если внутренняя функция использует this
, а мы вызовем её как функцию первого класса, то почувствуем на себе весь гнев утечки абстракции.
var fs = require('fs');
// страшновато
fs.readFile('freaky_friday.txt', Db.save);
// не так страшно
fs.readFile('freaky_friday.txt', Db.save.bind(Db));
Вызвав bind
, мы даём объекту Db
возможность использовать мусорный код из его прототипа. Я стараюсь избегать this
как грязных подгузников, да и в нём нет никакой необходимости, когда пишешь функциональный код. Однако, если вы собираетесь использовать внешние библиотеки, то не забывайте про безумный мир вокруг вас.
Некоторые могут поспорить, утверждая что this
необходим с точки зрения производительности. Если вы из микро-оптимизаторов, пожалуйста, закройте эту книгу. Если вам не удастся вернуть за неё деньги, я надеюсь, вы сможете её обменять на что-нибудь посложнее.
Теперь мы готовы продолжать.