Код развивается со временем. В частности, JavaScript сильно изменился за последние пару лет. Может быть сложно не отставать от всех новых функций языка! Это помогает увидеть конкретные примеры. В этом сообщении в блоге я рассмотрю гипотетический (но реалистичный) пример того, как обычная функция JavaScript ES3, подверженная жесткой среде «меняющихся требований», может использовать функции ES2015 + JavaScript, чтобы стать окончательной версией самой себя!

Привет, моя маленькая функция

function greet (firstname, lastname) {
    return "Hello " + firstname + " " + lastname;
}

Этого было достаточно для веб-сайта Imaginary Company в начале 2000-х годов. Но теперь Imaginary Company стала глобальной, и начальник попросил нас обновить функцию greet. Она хочет, чтобы greet автоматически переводился на нужный пользователю язык. Мы находчивы, но также ленивы, поэтому решили использовать Google Translate.

Обещай мне поздороваться

(Прежде чем вы спросите, да, google-translate-api - это настоящий пакет для npm. Я сказал вам, что это будет реалистично!)

const translate = require('google-translate-api');
async function greet (firstname, lastname, lang) {
    let greeting = await translate('Hello', {to: lang});
    return greeting + " " + firstname + " " + lastname;
}

Вот первое большое изменение: функция Google Translate возвращает обещание. Наш босс уже сказал нам прекратить использовать обратные вызовы, так что это означает, что наша функция greet должна также возвращать Promise. К счастью, работать с Promises сейчас не так сложно благодаря асинхронным функциям, которые были официально добавлены в JavaScript в ES2017.

Асинхронные вызовы заразительны - как только вы вводите асинхронную функцию в свой код, все, что использует эту функцию, становится асинхронным! Поэтому я предлагаю вам принять это на раннем этапе вашего дизайна. Даже если ваша функция на самом деле синхронна, вы можете прикрепить перед ней async, и теперь вы подготовили ее для будущего. Если вам когда-нибудь понадобится изменить реализацию, и она станет асинхронной, ничего страшного, потому что весь ваш существующий код уже вызывает ее с помощью await.

Теперь к нам приходит начальник с очередной просьбой. Иногда родной язык пользователя неизвестен. Она говорит нам, что функция должна по умолчанию использовать английский, если параметр языка не определен.

Не определено - не проблема

async function greet (firstname, lastname, lang = 'en') {
...

Это было просто! JavaScript поддерживает параметры по умолчанию с ES2015.

Раньше вам требовался такой код в вашей функции для реализации значений по умолчанию:

arg = arg || 'foo'

Но у этого был неудачный крайний случай, когда он не работал, если arg был равен 0 или false. Поэтому на всякий случай нам посоветовали провести более интересную проверку, например:

arg = typeof arg!=="undefined" && arg!==null ? arg : 'foo'

чего, конечно же, большинство людей никогда не делали. Но теперь это легко! Установка значений по умолчанию в объявлении функции удобна для чтения, и IDE также покажут эту информацию.

Когда мы чувствуем себя умными, начальник добавляет новое требование. В некоторых языках используется другой порядок имен. Она говорит, что имя и фамилия отсутствуют. Теперь это «имя» и «фамилия», и мы определим, какое первое, а какое последнее, на основе флага.

Порядок и хаос

async function greet (given, family, lang = 'en', reverse = false) {
...

К сожалению, наше наивное решение привело к проблеме. Есть два способа создать правильно выглядящие имена:

greet(given, family, 'en', false) // correct
greet(family, given, 'en', true) // correct

но есть и два способа облажаться:

greet(given, family, 'en', true) // wrong
greet(family, given, 'en', false) // wrong

Наша команда разработчиков разбросана по всему миру, и некоторые разработчики привыкли указывать фамилию перед именем, а некоторые - перед фамилией. Таким образом, у них возникают проблемы с запоминанием, означает ли true сначала фамилию, либо имя.

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

Объект разрушения

В некоторых языках, таких как Python, есть именованные аргументы. Именованные аргументы - это супер-круто, потому что вам не нужно помнить, в каком порядке они находятся, и тому, кто должен читать код, очевидно, что это за аргументы (то, что некоторые могут нагло назвать самодокументирующимся кодом). Используя деструктуризацию объектов JavaScript, мы можем добиться чего-то очень похожего на именованные аргументы.

async function greet ({given, family, lang='en', reverse=false}) {

Все, что мы сделали, это добавили пару фигурных скобок, но внезапно характер функции изменился. Теперь он принимает единственный аргумент, и этот аргумент является объектом:

greet({given: 'John', family: 'Smith'}) // 'Hello John Smith'
greet({family: 'Smith', given: 'John'}) // also 'Hello John Smith'

Боссу это нравится! Его легче читать, и никто больше не путает имя и фамилию.

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

let given = 'John'
let family = 'Smith'
let lang = 'en'
let reversed = false
greet({ lang, given, family, reversed }) // 'Hello John Smith'

Они чувствуют себя довольно умными. Боссу нравится, что все начинают использовать одни и те же имена переменных для данных во всем приложении; желание сохранить код «DRY» наконец побудило других разработчиков обновить старый код, который все еще использовал переменные с именами firstname и lastname или firstName и lastName для использования given и family.

Но теперь есть потенциальная ошибка нового типа. Если lang или reversed написаны неправильно, greet не увидит эти свойства и вместо этого будет использовать значение по умолчанию. Вы понимаете, что вместо беспокойства о том, в каком порядке находятся аргументы, вы потеряли беспокойство по поводу их написания.

Никаких опечаток

Мы можем решить эту проблему с помощью другой функции ES2018: rest properties!

async function greet ({
    given,
    family,
    lang = 'en',
    reverse = false,
    ...misspellings
}) {

Здесь ...misspellings будет перехватывать любые аргументы, НЕ совпадающие с остальной частью деструктуризации. Затем мы можем добавить простую проверку времени выполнения, чтобы убедиться, что функция вызывается правильно:

misspellings = Object.keys(misspellings)
if (misspellings.length > 0) {
  throw new Error('Unrecognized arguments: ' +
                  misspellings.join(','))
}

Теперь, если разработчик неправильно напишет один из аргументов, функция выдаст ошибку:

greet({ lang, given, family, reserved })
// Error: Unrecognized arguments: reserved

Доработанная функция

В этом сообщении в блоге я попытался показать, как простая функция

function greet (firstname, lastname) {
    return "Hello " + firstname + " " + lastname;
}

может развиваться в ответ на требования добавлять новые функции и удовлетворять новые требования.

Вот окончательный результат:

const translate = require('google-translate-api');
async function greet ({
    given,
    family,
    lang = 'en',
    reverse = false,
    ...misspellings
}) {
    // Check arguments
    misspellings = Object.keys(misspellings)
    if (misspellings.length > 0) {
      throw new Error('Unrecognized arguments: ' +
                      misspellings.join(','))
    }
    // Await translation from Google
    let greeting = await translate('Hello', {to: lang});
    // Determine name order
    if (reverse) {
        return greeting + " " + family + " " + given;
    } else {
        return greeting + " " + given + " " + family;
    }
}

Наша последняя функция использует преимущества многих современных «сверхспособностей» JavaScript:

  • асинхронные функции
  • параметры по умолчанию
  • деструктуризация объекта
  • остальные свойства

Я начал писать все свои функции, используя эти возможности, потому что они обеспечивают гибкую основу для адаптации функции к любым новым требованиям. Как выглядит ваша конечная функция?

P.S. Если вам нравится JavaScript, Stoplight нанимает!