Код развивается со временем. В частности, 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 нанимает!