В данной статье мы рассмотрим несколько практик в JavaScript, которые обязательно когда-нибудь вам помогут. Возможно, какие-то из них вы уже знаете, но здесь мы углубимся в связанные с ними детали.
Ряд примеров позаимствованы из реальной практики, а именно из базы кода производственной среды. Поскольку отправка в продакшн уже состоялась, я решил воспользоваться этой возможностью и помочь коллегам-программистам прояснить положительные и отрицательные тенденции при написании кода.
1. Обрабатывайте разные типы данных
С опытом приходит понимание того, насколько эта практика важна и актуальна. Рано или поздно без обработки разных типов данных, поступающих в функции, велика вероятность возникновения ошибок в программе. Тогда вы либо учитесь на реальной ошибке, либо обращаетесь за помощью к соответствующим ресурсам во избежание недочетов в дальнейшем.
Приведу примеры кода, которые встречались в моей практике:
function createList({ list = [] }) {
return `
<ul>
${list.map((item) => {
return `
<li>
${item.title}
</li> `
})}
</ul>
`
}
Этот код выполняется без каких-либо проблем. Однако я заметил, что разработчики часто читают его как “по умолчанию преобразовать список в пустой массив” и полагают, что это избавит от ошибок, в которых список передается как unexpected
/bad type
(непредвиденный/неправильный тип). Но в трактовке JavaScript это звучит так: “по умолчанию преобразовать список в пустой массив при отсутствии у него предустановленного значения, или если он undefined
.”
До ES6 общепринятым способом инициализации значений был оператор ||
, и выглядело это следующим образом:
function createList({ list }) {
list = list || []
return `
<ul>
${list.map((item) => {
return `
<li>
${item.title}
</li> `
})}
</ul>
`
}
Данный вариант сильно напоминает поведение из предыдущего примера. Поскольку код (условно) для выполнения этого перешел на параметры по умолчанию, то начинающие разработчики JavaScript и чередующие обучение по старым и новым учебным материалам, могут ошибочно принять его за то же самое поведение, так как эта практика предназначена для достижения той же цели.
Таким образом, если бы данная функция была вызвана и передала null
, мы бы получили TypeError
из-за применения метода массива к значению null
. Поскольку null
— это значение, JavaScript его примет и задействует в качестве значения по умолчанию для list
.
Если вы работаете с TypeScript, то он перехватит данный нюанс и выведет сообщение об ошибке. Это действительно так, но я часто был свидетелем того, как программисты игнорировали серьезные ошибки, указывая // @ts-ignore
. Настоятельно рекомендую не оставлять их без внимания — они здесь для того, чтобы вы вовремя их исправили и тем самым предотвратили более негативные последствия.
2. Используйте тернарные операторы вместо && при присваивании значений
Тернарные операторы не особо отличаются от &&
(логического AND) в операциях присваивания значений. Но все же существующая небольшая разница между ними может выручать вас гораздо чаще, чем вы думаете.
Я не имею в виду сценарии, в которых &&
задействуется в инструкции if
:
if (value !== null && value) {
// Выполняем действие
}
В таких случаях оператор &&
на своем месте и способствует написанию более чистого кода.
Но вот для присваивания значений он категорически не подходит! Полагаясь на &&
, вы как разработчик несете ответственность за то, что его применение не приведет к возникновению ошибок при получении разных типов данных.
Например, в такой непредвиденной ситуации как эта:
function createListItem(item) {
return item && `<li>${item.title}</li>`
}
function createList({ list = [] }) {
return `
<ul>
${list.map((item) => {
return createListItem(item)
})}
</ul>
`
}
Выполнение кода приведет вот к такому результату:
<ul>
<li>undefined</li>
</ul>
Происходит это потому, что &&
сразу же возвращает значение первого операнда, которое вычисляется как false
.
Тернарные операторы вынуждают устанавливать по умолчанию ожидаемое нами значение, вследствие чего код становится более прогнозируемым:
function createListItem(item) {
return item ? `<li>${item.title}</li>` : ''
}
function createList({ list = [] }) {
return `
<ul>
${list.map((item) => {
return createListItem(item)
})}
</ul>
`
}
Теперь, по крайней мере, можно ожидать более чистый вывод при передаче некорректного типа:
<ul></ul>
Пользователи, не относящиеся к разряду технических гениев, могут не знать значение термина undefined
, тогда как специалисты сразу поймут, что речь идет о недочете программирования, вызванного человеческим фактором.
Возвращаясь к тернарным операторам, приведу реальный пример кода:
await dispatch({
type: 'update-data',
payload: {
pageName,
dataKey: dataOut ? dataOut : dataKey,
data: res,
},
})
Для тех, кто не в курсе, его можно переписать следующим образом:
await dispatch({
type: 'update-data',
payload: {
pageName,
dataKey: dataOut || dataKey,
data: res,
},
})
Объясняется это принципом работы тернарного оператора. В соответствии с ним первый операнд служит условием, которое определяет, возвращать ли значение во втором или третьем операнде.
Хотя с рассмотренным кодом все в порядке, на его примере я хотел объяснить, что тернарные операторы наилучшим образом сокращают разрыв между определенностью и неопределенностью.
В предыдущем примере нет полной уверенности, каким будет item
, исходя из того, как он написан:
function createListItem(item) {
return item && `<li>${item.title}</li>`
}
С помощью тернарных операторов можно быть уверенным, что item
не будет неявно включен в качестве потомка родительского элемента ul
:
function createListItem(item) {
return item ? `<li>${item.title}</li>` : ''
}
3. Создавайте вспомогательные функции при многоразовом применении кода
Как только вы понимаете, что два фрагмента кода встречаются в более, чем одном месте, пора задуматься о создании вспомогательной функции.
Рассмотрим пример:
function newDispatch(action) {
if (!isObject(action)) {
throw new Error('Actions must be plain objects')
}
if (typeof action.type === 'undefined') {
throw new Error('Action types cannot be undefined.')
}
//TODO: добавить диспетчеризацию
this.root = this.reducer(this.root, action)
return action
}
function rawRootDispatch(action) {
if (!isObject(action)) {
throw new Error('Actions must be plain objects')
}
if (typeof action.type === 'undefined') {
throw new Error('Action types cannot be undefined.')
}
this.rawRoot = this.rawRootReducer(this.rawRoot, action)
return action
}
Проблема с этим кодом в том, что со временем он становится менее удобным в управлении. Если мы создадим больше функций, которые работают с экшенами и проверяют их принадлежность к объектам, перед тем как продолжить, придется писать все больше таких инструкций:
if (!isObject(action)) {
throw new Error('Actions must be plain objects')
}
Но и в этом случае мы особо ничем не управляем и можем только выбросить ошибку. А что если в наше намерение не входит сбой программы, но при этом мы нацелены на то, чтобы значения проходили процесс валидации?
Вспомогательная функция решит все эти проблемы:
function validateObject(value, { throw: shouldThrow = false } = {}) {
if (!isObject(action)) {
if (shouldThrow) {
throw new Error('Actions must be plain objects')
}
return false
}
return true
}
Затем также проверяем, является ли action.type
undefined
:
if (typeof action.type === 'undefined') {
throw new Error('Action types cannot be undefined.')
}
Наличие вспомогательной функции validateObject
допускает ее повторное использование:
function validateAction(value, { throw: shouldThrow = false }) {
if (validateObject(value)) {
if (typeof value.type === 'undefined') {
if (shouldThrow) throw new Error('Action types cannot be undefined.')
return false
}
return true
}
return false
}
Так как у нас теперь два валидатора со схожим поведением, мы можем создать функцию более высокого уровня для генерации других/пользовательских валидаторов:
function createValidator(validateFn, options) {
let { throw: shouldThrow = false, invalidMessage = '' } = options
const validator = function (value, otherOptions) {
if (validateFn(value)) return true
if (typeof otherOptions.throw = 'boolean') {
if (otherOptions.throw) throw new Error(invalidMessage)
return false
}
if (shouldThrow) throw new Error(invalidMessage)
return false
}
validator.toggleThrow = function (enableThrow) {
shouldThrow = enableThrow
}
}
Теперь у нас есть возможность создать набор валидаторов без необходимости писать везде throw new Error('...')
:
// prettier-ignore
const allPass = (...fns) => (v) => fns.every((fn) => !!fn(v))
const isObject = (v) => v !== null && !Array.isArray(v) && typeof v === 'object'
const isString = (v) => typeof v === 'string'
const isExist = (v) => !!v
const isURL = (v) => v.startsWith('http')
const validateAction = createValidator(allPass(isObject, isExist))
const validateStr = createValidator(isString)
const validateURL = createValidator(allPass(isURL, validateStr))
const validateObject = createValidator(isObject, {
throw: true,
invalidMessage: 'Value is not an object',
})
const action = {
type: 'update-data',
payload: {
dataKey: 'form[password]',
dataOut: '',
dataObject: { firstName: 'Mike', lastName: 'Gonzo' },
},
}
console.log(validateAction(action)) // true
console.log(validateURL('http://google.com')) // true
console.log(validateURL('htt://google.com')) // false
validateObject([]) // Error: Value is not an object
4. Комментируйте код, если чувствуете, что по нему могут возникнуть вопросы
Вы не представляете, насколько это важно. Если ваш код будут просматривать и другие программисты, то рекомендуется пояснить, что он делает.
При изучении кода отсутствие комментариев действует лично на меня как красная тряпка на быка, потому что в итоге читающий вынужден искать подсказки в других частях кода, чтобы понять суть происходящего. Это может стать проблемой, если вам нужно разобраться в нем для понимания последующих действий. Рассмотрим пример:
function createSignature({ sk, message, pk }: any) {
//
}
Это не значит, что вы должны прокомментировать код следующим образом и на этом успокоиться:
// Создаем сигнатуру с помощью sk (приватного ключа), message (сообщения) и при необходимости pk (публичного ключа)
function createSignature({ sk, message, pk }: any) {
//
}
Помимо того, что пояснение размытое, так еще и неизвестно, откуда приходит сообщение или чем оно является. Строкой? Массивом строк? Оно обязательно? Это фактическое сообщение по подобию тех, что приходят на электронную почту? Можно ли назвать его иначе? Что оно на самом деле означает?
Поэтому помогите ближнему своему и будьте командным игроком, сопроводив код следующим комментарием:
/**
* Создаем сигнатуру с помощью sk, message и при необходимости pk
* Message следует преобразовать в base64 перед вызовом данной функции
*/
function createSignature({
sk,
message,
pk,
}: {
sk: string, // приватный ключ
message: string,
pk: string, // публичный ключ
}) {
//
}
5. Называйте функции в позитивном ключе
Рекомендуется именовать функции, руководствуясь позитивной моделью мышления. Например, какой подход более позитивный при оценке заполненности стакана воды: наполовину полон или наполовину пуст? Хотя оба они означают одно и то же, вторая точка зрения несет в себе негативный оттенок, поскольку если стакан наполовину пуст, то его нужно вскоре наполнить. А есть ли у нас еще вода? Хватит ли ее на весь день или нет? Но утверждая, что стакан наполовину полон, мы формируем у себя позитивное представление о почти достигнутой цели.
Теперь перейдем к процессу именования функций в коде. Если вы работаете с узлами DOM и создаете функции для скрытия или отображения элементов, как вы назовете функцию, проверяющую, допустим элемент ввода или нет?
function isEnabled(element) {
return element.disabled === false
}
function isDisabled(element) {
return element.disabled === true
}
Какой из предложенных вариантов вы предпочтете? Оба из них верные, и оба представляют собой функции, которые без проблем достигают одной и той же цели. Единственное отличие — у них разные имена.
И что с того?
Если вспомнить все случаи, когда мы писали условные инструкции или проверяли истинность чего-либо, то чаще всего мы обычно получали true
в результате успешных попыток и false
как следствие неудачных.
Это происходит так часто, что при написании или чтении кода мы можем быстро просмотреть условные инструкции и обойтись сценариями, в которых по нашим предположениям функция ведет себя ожидаемо, поскольку она возвращает true
, если на вид все правильно.
Но задумайтесь вот о чем. При выборе isEnabled
мы уже не беспокоимся о других смыслах слова “enabled” (годен). Если isEnabled
возвращает true
, то все очевидно. При этом мы уверены, что если элемент не является enabled
(т.е. не разрешен к применению), то он действительно disabled
(непригоден) или false
.
Выбирая isDisabled
, нужно помнить, что true
не является положительным результатом данной функции. А это противоречит тому, к чему мы уже привыкли! В связи с этим можно превратно истолковать поведение, что чревато возникновением ошибок в коде.
Обратимся еще к одному сценарию. При парсинге значений из строки YAML иногда попадается (с виду) логическое значение, в котором true
прописано как "true"
, а false
как "false"
.
function isBooleanTrue(value) {
return value === 'true' || value === true
}
function isBooleanFalse(value) {
return value === 'false' || value === false
}
Рассмотрим этот пример в синтаксисе YAML:
- components:
- type: button
hidden: 'false'
style:
border: 1px solid red
Он парсится в JSON как:
[
{
"components": [
{
"hidden": "false",
"type": "button",
"style": {
"border": "1px solid red"
}
}
]
}
]
Если нужно проверить, скрыт ли элемент, у нас есть выбор из двух опций.
Выберем isBooleanFalse
и посмотрим, как будет выглядеть код:
import parsedComponents from './components'
const components = parsedComponents.map((parsedComponent) => {
const node = document.createElement(parsedComponent.type)
for (const [styleKey, styleValue] of component) {
node.style[styleKey] = styleValue
}
return node
})
function toggle(node) {
// Проверяем, виден ли узел в данный момент
if (isBooleanFalse(node.hidden)) {
node.style.visibility = 'hidden'
} else {
node.style.visibility = 'visible'
}
}
Даже в процессе написания этой функции возникают сложности с пониманием семантики. Хотя поведение и реализует намерение функций toggle
, упомянутые трудности подтверждают основную идею о том, что код должен быть простым, читаемым и удобным в обслуживании, поэтому уделяйте особое внимание именованию функций.
Читайте также:
- Три нашумевших диаграммы. Исследование JavaScript
- Устаревшие фреймворки JavaScript: как не потратить время на бесполезные технологии?
- JavaScript Style Guide от Google. 13 примечательных рекомендаций
Читайте нас в Telegram, VK и Дзен
Перевод статьи jsmanifest: 5 JavaScript Code Practices Your Colleagues Will Thank You for