Шаблоны ES6: Чистые функции высших порядков
JavaScript

Шаблоны ES6: Чистые функции высших порядков

Оригинал: ES6 Patterns: Clean Higher Order Functions, Ben McCormick
Я пишу код, используя новые возможности ECMAScript 2015 (более известного как ES6) с января. В течение года я использовал несколько новых шаблонов, которые, я думаю, делают мой код лучше. Я хочу поделиться ими в небольших статьях и начну с написания функций высшего порядка.

Функция высшего порядка — это функция, удовлетворяющая хотя бы одному из условий:

  • Принимает другую функцию в качестве аргумента
  • Возвращает функцию при вызове

Функции высшего порядка являются ключевой частью JavaScript, разработчики сталкиваются с ними каждый день, хотя сам термин некоторые могут не знать. jQuery, Underscore и другие библиотеки используют функции высшего порядка. Многие из основных JavaScript API, такие как map, filter, forEach и setTimeout принимают функции в качестве аргументов. Если вы слабо знакомы с этой концепцией, изучите подробнее, это поможет лучше понимать экосистему JavaScript. Надеюсь, эта статья вызовет у вас дополнительный интерес.

В шаблоне, который я буду описывать, я сфокусируюсь на втором типе функций высшего порядка — фунциях, который возвращают другие функции. Они могут быть полезны в различных случаях. В качестве примера давайте рассмотрим приложение для комментирования записей блога со списком пользователей. Наша задача — получить список комментариев для каждого пользователя. Мы можем добиться этого, скомбинировав функции map, filter и простую вспомогательную функцию, вроде этой:


import { commentList } from './comments';

function getCommentFromUser(userId) {  
    return {
        user: userId,
        comments:commentList.filter(function (comment) {
            return comment.user === userId 
        },
    };
}

let userIds = [1, 4, 5],  
    userComments = userIds.map(getCommentFromUser);

// делает что-то с комментариями

Мы написали довольно простой работающий код: берем список идентификаторов пользователей, а возвращаем список пользователей с их комментариями. Все хорошо, мы можем двигаться дальше, делать что-то с комментариями. Если мы не собираемся поддерживать этот код, на этом моменте можно остановиться.

Проблема заключается в том, что данный пример не очень гибок, несмотря на то, что он прекрасно работает для простого случая. Мы получаем комментарии из одного источника, который сделали доступным во всем модуле. Мы не можем добавить дополнительные фильтры к комментариям или использовать другой источник, не переписывая существующий код.

Если мы захотим, скажем, получить комментарии за прошлый год, мы должны будем либо изменить список commentList, либо переписать функцию getCommentFromUser, что может оказать влияние на остальной код. Это выглядит довольно некрасиво, поскольку отображение комментариев на пользователей не должно зависеть от самих комментариев.

К счастью, мы можем решить эту проблему с помощью функции высшего порядка. Если мы перепишем getCommentFromUser таким образом, чтобы она принимала список комментариев, а возвращала функцию от id пользователя, мы сможем контролировать источник комментариев. В примере ниже мы по прежнему используем полный список, но теперь его можно отфильтровать или получить комментарии из нескольких источников, не затрагивая код функции getCommentFromUser.


import { commentList } from './comments';

function getCommentFromUser (comments)  
    return function (userId) {
        return {
            user: userId,
            comments: comments.filter(function (comment) {
                return comment.user === userId 
            },
        };
    }
}

let userIds = [1, 4, 5],  
    userComments = userIds.map(getCommentFromUser(commentList));

Код стал гораздо более гибким, но ценой читаемости. Разработчикам без существенного опыта функционального программирования будет сложно читать четыре уровня вложенности и несколько выражений return. Пока мы пытались сделать получение списка комментариев более гибким, мы усложнили саму функцию. Это крайне не дружелюбно для новичков. В то же время, использование функций высшего порядка — это полезный шаблон, действительно необходимый в разработке приложения. К счастью, ES6 предоставляет нам возможность очистить синтаксис.

Стрелочные функции (arrow functions) ES6 имеют два основных отличия от ключевого слова function. Во-первых, они не связывают значение this с контекстом, вместо этого используют this из родительской области видимости. Во-вторых, они могут быть записаны в одну строчку, без фигурных скобок и явного вызова return. Т.е. вы можете написать функцию, возвращающую квадрат числа таким образом:


const square = (x) => x * x;

Первая особенность этих функций уже описана несколько раз в различных источниках. Но для нашего примера это не принципиально, поскольку мы не используем this. В нашем случае важно то, что стрелочная функция позволяет просто записать функцию высшего порядка. Со стрелочными функциями наш пример будет выглядеть так:


import { commentList } from './comments';

const getCommentFromUser = (comments) => (userId) => ({  
    user: userId,
    comments: comments.filter( (comment) => comment.user === userId),
})

let userIds = [1, 4, 5],  
    userComments = userIds.map(getCommentFromUser(commentList));

Мне нравится шаблон двойной функции () => () =>, я заметил, что часто использую его, особенно с функциями map, filter и reduce. Преобразования массивов, использующие эти функции часто требуют какой-либо контекстной информации, не содержащейся в массиве. Простой способ заключается в использовании переменных из родительской области видимости, как мы сделали в первом примере.

Мне кажется, гораздо лучше использовать функцию высшего порядка, которая принимает переменные, необходимые для трансформации, и возвращает новую функцию, которую можно применить для трансформации. Это уменьшает количество вещей, которые могут повлиять на результат функции, повышает гибкость без необходимости изменения логики трансформации, дает понять читающему, какие значения использует функция. А двойные функции упрощают этот способ, они стали одним из моих любимых шаблонов ES6.

Дополнительные материалы

Если вы интересуетесь функциями высшего порядка в JavaScript, я не знаю лучшего источника, чем JavaScript Alonge. Эта книга является фантастическим погружением в JavaScript с функциональным уклоном, формирует понимание снизу вверх и предлагает множество идей и рецептов. К тому же она была недавно переписана, чтобы отразить изменения в языке, пришедшие с ES6.

Глубокое погружение в стрелочные функции на 2ality Акселя Раушмайера. Если вы не знакомы с этим блогом, обязательно посмотрите, это отличный источник информации по ES6 и JavaScript в целом.

Рассылка
Подпишитесь на рассылку и получайте дайджест новостей и статей.
Никакого спама!
Подписаться