Попрощайтесь с вложенностью функций в JavaScript

30 Nov, 2023

Репозиторий давно не обновлялся

Последнее обновление было 2 года назад.

7,897
🔱 106
👥 256

Знакома ситуация? Вы берете данные, применяете одну функцию, к результату — другую, потом третью... и вот уже ваш код напоминает матрешку или пирамиду из скобок. Читать такое — мука, а вносить правки — еще хуже.

// Узнали? Заболели?
three(two(one(value)));

С другой стороны, есть элегантные цепочки методов, как в jQuery или Lodash:

value.one().two().three();

Красиво, линейно, понятно. Но есть проблема: так можно делать, только если one, two и three — это методы объекта value. А что, если это обычные функции? Или вам нужно смешать вызовы методов с арифметикой, созданием объектов и await?

Именно эту головную боль и призван решить оператор конвейера (|> или pipe operator) — одно из самых ожидаемых нововведений в JavaScript, которое сейчас находится на 2 стадии рассмотрения в комитете TC39. Давайте разберемся, как он работает и почему за ним стоит следить.

Что такое Pipe Operator и зачем он нужен?

Если коротко, pipe-оператор |> — это синтаксический сахар, который позволяет передавать результат одного выражения в качестве аргумента для следующего, делая код более линейным и читаемым.

Реклама

Он берет лучшее от двух миров:

  1. Универсальность вложенных вызовов: работает с любыми функциями, методами, операциями.
  2. Читаемость цепочек методов: код выполняется слева направо, как мы привыкли читать.

Давайте посмотрим на реальный пример из кодовой базы React. Вот так выглядит код без оператора конвейера:

console.log(
  chalk.dim(
    `$ ${Object.keys(envars)
      .map(envar =>
        `${envar}=${envars[envar]}`)
      .join(' ')
    }`,
    'node',
    args.join(' ')
  )
);

Чтобы понять, что здесь происходит, приходится буквально "нырять" вглубь скобок, находить самую вложенную часть (envars) и мысленно раскручивать клубок обратно.

А теперь магия. Вот как этот же код преображается с использованием pipe-оператора:

Object.keys(envars)
  .map(envar => `${envar}=${envars[envar]}`)
  .join(' ')
  |> `$ ${%}`
  |> chalk.dim(%, 'node', args.join(' '))
  |> console.log(%);

Разница колоссальная! Мы сразу видим исходные данные и последовательно, шаг за шагом, следим за их трансформацией. Код читается как рассказ.

Как это работает? Ключевые детали

Основная идея проста: значение |> операция. Выражение слева от |> вычисляется, и его результат "вставляется" в выражение справа.

Специальный токен %

Куда именно вставляется результат? Для этого используется специальный токен-плейсхолдер — %. Он указывает место, куда подставится значение из предыдущего шага.

Интересно, что % — это временный вариант. Комитет еще обсуждает, какой символ будет финальным, но пока остановились на этом.

Гибкость — его второе имя

Самое крутое в этом подходе (который называется "Hack-style pipes") — его невероятная гибкость. Плейсхолдер % можно использовать практически где угодно, где может стоять переменная:

  • Вызов обычной функции: value |> myFunc(%)
  • Вызов с несколькими аргументами: value |> myFunc(10, %, 'hello')
  • Вызов метода: value |> %.myMethod()
  • Арифметика: value |> % + 10
  • Создание массива: value |> [%, 'anotherValue']
  • Создание объекта: value |> { data: % }
  • Шаблонные строки: value |> `Результат: ${%}`
  • Работа с await: promise |> await %

Эта универсальность и делает оператор таким мощным. Вам не нужно оборачивать все в функции — вы просто пишете код, как привыкли, но в линейном стиле.

Почему не просто временные переменные?

Скептик скажет: "Зачем новый оператор? Я могу просто разбить все на переменные!"

const keys = Object.keys(envars);
const mapped = keys.map(envar => `${envar}=${envars[envar]}`);
const joined = mapped.join(' ');
const template = `$ ${joined}`;
const dimmed = chalk.dim(template, 'node', args.join(' '));
console.log(dimmed);

Да, так можно. Но это порождает другие проблемы:

  1. Много "мусора". Появляется куча одноразовых переменных, которые загромождают область видимости и усложняют чтение. Не зря говорят, что одно из самых сложных дел в программировании — давать хорошие имена.
  2. Утомительно. Писать const name = ... для каждого мелкого шага попросту долго и нудно.
  3. Не всегда возможно. Pipe-оператор — это выражение (expression), а не инструкция (statement). Это значит, что его результат можно сразу вернуть из функции или использовать, например, в JSX, чего не сделаешь с последовательностью const.

Битва пайплайнов: Hack против F#

Важно знать, что путь оператора в стандарт был тернист. Долгое время существовало два конкурирующих предложения:

  1. Hack pipes (тот, что побеждает): value |> func(%). Гибкий, универсальный, но требует явно указывать вызов (%) даже для функций с одним аргументом.
  2. F# pipes: value |> func. Очень лаконичен для унарных функций (с одним аргументом). Но для всего остального (методы, арифметика, функции с несколькими аргументами) требует обертки в стрелочную функцию: value |> (x => x.method()), value |> (x => x + 10).

Комитет TC39 дважды отклонял F#-стиль. Основные причины — он менее универсален для типичного JavaScript-кода, где вызовы методов и функций с несколькими аргументами встречаются чаще, чем унарные. К тому же, F#-стиль мог приводить к трудноуловимым ошибкам во время выполнения, если справа оказывалось не то, что ожидалось.

В итоге выбор пал на Hack-стиль как на более прагматичный и безопасный вариант, который лучше вписывается в существующую экосистему языка.

Что в итоге?

Оператор конвейера (|>) — это не просто синтаксическая безделушка. Это мощный инструмент для написания более чистого, понятного и поддерживаемого кода. Он устраняет "лесенки" из вложенных вызовов и позволяет мыслить в терминах потока данных: взяли данные, сделали раз, сделали два, получили результат.

Сейчас предложение находится на стадии 2, а это значит, что его концепция уже утверждена, и идет активная работа над деталями спецификации. Финальный синтаксис еще может немного измениться, но общая идея останется прежней.

Хотите попробовать уже сегодня? Вы можете! Оператор доступен через плагин для Babel: @babel/plugin-proposal-pipeline-operator.

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

🍪 Мы используем файлы cookie и сервис аналитики Яндекс.Метрика, чтобы сайт работал лучше. Продолжая пользоваться devtrends.ru, вы соглашаетесь с обработкой данных согласно Политике конфиденциальности.