Skip to content

Latest commit

 

History

History
141 lines (97 loc) · 10.7 KB

ch02-ru.md

File metadata and controls

141 lines (97 loc) · 10.7 KB

Глава 02: Функции первого класса

Краткий обзор

Когда мы говорим о функциях «первого класса», то имеем в виду, что они того же класса, как и все остальные значения. С функциями можно обращаться так же, как и с любым другим типом данных, и в них нет ничего особенного: их можно хранить в массивах, передавать в другие функции как аргументы, использовать в качестве значений переменных, — всё, что душе угодно.

Это азы 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 необходимо с точки зрения производительности. Если вы из микро-оптимизаторов, пожалуйста, закройте эту книгу. Если вам не удастся вернуть за неё деньги, то, я надеюсь, сможете её обменять на что-нибудь более замороченное.

Теперь мы готовы продолжать.

Глава 03: Чистое счастье с Чистыми функциями