[Перевод] Руководство по JavaScript, часть 6: исключения, точка с запятой, шаблонные литералы
Темами сегодняшней части перевода руководства по JavaScript станут обработка исключений, особенности автоматической расстановки точек с запятой и шаблонные литералы.
→ Часть 1: первая программа, особенности языка, стандарты
→ Часть 2: стиль кода и структура программ
→ Часть 3: переменные, типы данных, выражения, объекты
→ Часть 4: функции
→ Часть 5: массивы и циклы
→ Часть 6: исключения, точка с запятой, шаблонные литералы
Обработка исключений
Когда при выполнении кода возникает какая-нибудь проблема, в JavaScript она выражается в виде исключения. Если не предпринять меры по обработке исключений, то, при их возникновении, выполнение программы останавливается, а в консоль выводится сообщение об ошибке.
Рассмотрим следующий фрагмент кода.
let obj = {value: 'message text'}
let notObj
let fn = (a) => a.value
console.log(fn(obj)) //message text
console.log('Before') //Before
console.log(fn(notObj)) //ошибка, выполнение программы останавливается
console.log('After')
Здесь у нас имеется функция, которую планируется использовать для обработки объектов, имеющих свойство value
. Она возвращает это свойство. Если использовать эту функцию по назначению, то есть — передать ей такой объект, на работу с которым она рассчитана, при её выполнении ошибок выдано не будет. А вот если передать ей нечто неподходящее, в нашем случае — объявленную, но неинициализированную переменную, то при попытке обратиться к свойству value
значения undefined
произойдёт ошибка. В консоль попадёт сообщение об ошибке, выполнение программы остановится.
Вот как это выглядит при запуске данного кода в среде Node.js.
Исключение TypeError в Node.js
Если нечто подобное встретится в JS-коде веб-страницы, в консоль браузера попадёт похожее сообщение. Если такое произойдёт в реальной программе, скажем — в коде веб-сервера, подобное поведение крайне нежелательно. Хорошо было бы иметь механизм, который позволяет, не останавливая программу, перехватить ошибку, после чего принять меры по её исправлению. Такой механизм в JavaScript существует, он представлен конструкцией try...catch
.
▍Конструкция try…catch
Конструкция try...catch
позволяет перехватывать и обрабатывать исключения. А именно, в неё входит блок try
, в который включают код, способный вызвать ошибку, и блок catch
, в который передаётся управление при возникновении ошибки. В блоки try
не включают абсолютно весь код программы. Туда помещают те его участки, которые могут вызвать ошибки времени выполнения. Например — вызовы функций, которым приходится работать с некими данными, полученными из внешних источников. Если структура таких данных отличается от той, которую ожидает функция, возможно возникновение ошибки. Вот как выглядит схема конструкции try...catch
.
try {
//строки кода, которые могут вызвать ошибку
} catch (e) {
//обработка ошибки
}
Если код выполняется без ошибок — блок catch
(обработчик исключения) не выполняется. Если же возникает ошибка — туда передаётся объект ошибки и там выполняются некие действия по борьбе с этой ошибкой.
Применим эту конструкцию в нашем примере, защитив с её помощью опасные участки программы — те, в которых вызывается функция fn()
.
let obj = {value: 'message text'}
let notObj
let fn = (a) => a.value
try {
console.log(fn(obj))
} catch (e) {
console.log(e.message)
}
console.log('Before') //Before
try {
console.log(fn(notObj))
} catch (e) {
console.log(e.message) //Cannot read property 'value' of undefined
}
console.log('After') //After
Посмотрим на результаты выполнения этого кода в среде Node.js.
Обработка ошибки в Node.js
Как видите, если сравнить этот пример с предыдущим, теперь выполняется весь код, и тот, что расположен до проблемной строки, и тот, что расположен после неё. Мы «обрабатываем» ошибку, просто выводя в консоль значения свойства message
объекта типа Error. В чём будет заключаться обработка ошибки, возникшей в реально используемом коде, зависит от ошибки.
Выше мы обсудили блок try...catch
, но, на самом деле, эта конструкция включает в себя ещё один блок — finally
.
▍Блок finally
Блок finally
содержит код, который выполняется независимо от того, возникла или нет ошибка в коде, выполняющемся в блоке try
. Вот как это выглядит.
try {
//строки кода
} catch (e) {
//обработка ошибки
} finally {
//освобождение ресурсов
}
Блок finally
можно использовать и в том случае, если в блоке try...catch...finally
отсутствует блок catch
. При таком подходе он используется так же, как и в конструкции с блоком catch
, например — для освобождения ресурсов, занятых в блоке try
.
▍Вложенные блоки try
Блоки try
могут быть вложены друг в друга. При этом исключение обрабатывается в ближайшем блоке catch
.
try {
//строки кода
try {
//другие строки кода
} finally {
//ещё какой-то код
}
} catch (e) {
}
В данном случае, если исключение возникнет во внутреннем блоке try
, обработано оно будет во внешнем блоке catch
.
▍Самостоятельное генерирование исключений
Исключения можно генерировать самостоятельно, пользуясь инструкцией throw
. Вот как это выглядит.
throw value
После того, как выполняется эта инструкция, управление передаётся в ближайший блок catch
, или, если такого блока найти не удаётся, выполнение программы прекращается. Значением исключения может быть всё что угодно. Например — определённый пользователем объект ошибки.
О точках с запятой
Использовать точки с запятой в JavaScript-коде необязательно. Некоторые программисты обходятся без них, полагаясь на автоматическую систему их расстановки, и ставя их только там, где это совершенно необходимо. Некоторые предпочитают ставить их везде, где это возможно. Автор этого материала относит себя к той категории программистов, которые стремятся обходиться без точек с запятой. Он говорит, что решил обходиться без них осенью 2017 года, настроив Prettier так, чтобы он удалял их везде, где без их явной вставки можно обойтись. По его мнению код без точек с запятой выглядит естественнее и его легче читать.
Пожалуй, можно сказать, что сообщество JS-разработчиков разделено, по отношению к точкам с запятой, на два лагеря. При этом существуют и руководства по стилю JavaScript, которые предписывают явную расстановки точек с запятой, и руководства, которые рекомендуют обходиться без них.
Всё это возможно из-за того, что в JavaScript существует система автоподстановки точек с запятой (Automatic Semicolon Insertion, ASI). Однако, то, что в JS коде, во многих ситуациях, можно обойтись без этих символов, и то, что точки с запятой расставляются автоматически, при подготовке кода к выполнению, не означает, что программисту не нужно знать правила, по которым это происходит. Незнание этих правил приводит к появлению ошибок.
▍Правила автоподстановки точек с запятой
Парсер JavaScript-кода автоматически добавляет точки с запятой при разборе текста программы в следующих ситуациях:
- Когда следующая строка начинается с кода, который прерывает текущий код (код некоей команды может располагаться на нескольких строках).
- Когда следующая строка начинается с символа
}
, который закрывает текущий блок. - Когда обнаружен конец файла с кодом программы.
- В строке с командой
return
. - В строке с командой
break
. - В строке с командой
throw
. - В строке с командой
continue
.
▍Примеры кода, который работает не так, как ожидается
Вот некоторые примеры, иллюстрирующие вышеприведённые правила. Например, как вы думаете, что будет выведено в результате выполнения следующего фрагмента кода?
const hey = 'hey'
const you = 'hey'
const heyYou = hey + ' ' + you
['h', 'e', 'y'].forEach((letter) => console.log(letter))
При попытке выполнения этого кода будет выдана ошибка Uncaught TypeError: Cannot read property 'forEach' of undefined
система, основываясь на правиле №1, пытается интерпретировать код следующим образом.
const hey = 'hey';
const you = 'hey';
const heyYou = hey + ' ' + you['h', 'e', 'y'].forEach((letter) => console.log(letter))
Проблему можно решить, самостоятельно поставив точку с запятой после предпоследней строки первого примера.
Вот ещё один фрагмент кода.
(1 + 2).toString()
Результатом его выполнения станет вывод строки "3"
. А что произойдёт, если нечто подобное появится в следующем фрагменте кода?
const a = 1
const b = 2
const c = a + b
(a + b).toString()
В данной ситуации появится ошибка TypeError: b is not a function
так как вышеприведённый код будет интерпретирован следующим образом.
const a = 1
const b = 2
const c = a + b(a + b).toString()
Взглянем теперь на пример, основанный на правиле №4.
(() => {
return
{
color: 'white'
}
})()
Можно подумать, что это IIFE вернёт объект, содержащий свойство color
, но на самом деле это не так. Вместо этого функция вернёт значение undefined
так как система добавляет точку с запятой после команды return
.
Для того чтобы решить подобную проблему, открывающую фигурную скобку объектного литерала нужно поместить в той же строке, где находится команда return
.
(() => {
return {
color: 'white'
}
})()
Если взглянуть на следующий фрагмент кода, то можно подумать, что он выведет в окне сообщения 0
.
1 + 1
-1 + 1 === 0 ? alert(0) : alert(2)
Но он выводит 2, так как, в соответствии с правилом №1, этот код представляется следующим образом.
1 + 1 -1 + 1 === 0 ? alert(0) : alert(2)
В вопросе использования точек с запятой в JavaScript стоит проявлять осторожность. Вы можете встретить как горячих сторонников точек с запятой, так и их противников. На самом деле, решая, нужны ли в вашем коде точки с запятой, можно положиться на тот факт, что JS поддерживает их автоматическую подстановку, но при этом каждый должен сам для себя решить — нужны ли они в его коде или нет. Главное — последовательно и разумно применять выбранный подход. В том, что касается расстановки точек с запятой и структуры кода, можно порекомендовать придерживаться следующих правил:
- Пользуясь командой
return
, располагайте то, что она должна вернуть из функции, в той же строке, в которой находится эта команда. То же самое касается командbreak
,throw
,continue
. - Уделяйте особое внимание ситуациям, когда новая строка кода начинается со скобки, так как эта строка может быть автоматически объединена с предыдущей и представлена системой как попытка вызова функции или попытка доступа к элементу массива.
В целом же можно сказать, что, ставите ли вы точки с запятой самостоятельно, или полагаетесь на их автоматическую расстановку, тестируйте код для того, чтобы убедиться, что работает он именно так, как ожидается.
Кавычки и шаблонные литералы
Поговорим об особенностях использования кавычек в JavaScript. А именно, речь идёт о следующих допустимых в JS-программах типах кавычек:
- Одинарные кавычки.
- Двойные кавычки.
- Обратные кавычки.
Одинарные и двойные кавычки, в целом, можно считать одинаковыми.
const test = 'test'
const bike = "bike"
Разницы между ними практически нет. Пожалуй, единственное заметное различие заключается в том, что в строках, заключённых в одинарные кавычки, нужно экранировать символ одинарной кавычки, а в строках, заключённых в двойные — символ двойной.
const test = 'test'
const test = 'te\'st'
const test = 'te"st'
const test = "te\"st"
const test = "te'st"
В разных руководствах по стилю можно найти как рекомендацию по использованию одинарных кавычек, так и рекомендацию по использованию двойных кавычек. Автор этого материала говорит, что в JS-коде стремится использовать исключительно одинарные кавычки, используя двойные только в HTML-коде.
Обратные кавычки появились в JavaScript с выходом стандарта ES6 в 2015 году. Они, помимо других новых возможностей, позволяют удобно описывать многострочные строки. Такие строки можно задавать и используя обычные кавычки — с применением escape-последовательности \n
. Выглядит это так.
const multilineString = 'A string\non multiple lines'
Обратные кавычки (обычно кнопка для их ввода находится левее цифровой клавиши 1 на клавиатуре) позволяют обойтись без \n
.
const multilineString = `A string
on multiple lines`
Но этим возможности обратных кавычек не ограничены. Так, если строка описана с использованием обратных кавычек, в неё, используя конструкцию ${}
, можно подставлять значения, являющиеся результатом вычисления JS-выражений.
const multilineString = `A string
on ${1+1} lines`
Такие строки называют шаблонными литералами.
Шаблонные литералы отличаются следующими особенностями:
- Они поддерживают многострочный текст.
- Они дают возможность интерполировать строки, в них можно использовать встроенные выражения.
- Они позволяют работать с тегированными шаблонами, давая возможность создавать собственные предметно-ориентированные языки (DSL, Domain-Specific Language).
Поговорим об этих возможностях.
▍Многострочный текст
Задавая, с помощью обратных кавычек, многострочные тексты, нужно помнить о том, что пробелы в таких текстах так же важны, как и другие символы. Например, рассмотрим следующий многострочный текст.
const string = `First
Second`
Его вывод даст примерно следующее.
First
Second
То есть оказывается, что когда этот текст вводился в редакторе, то, возможно, программист ожидал, что слова First
и Second
, при выводе, окажутся строго друг под другом, но на самом деле это не так. Для того чтобы обойти эту проблему, можно начинать многострочный текст с перевода строки, и, сразу после закрывающей обратной кавычки, вызывать метод trim()
, который удалит пробельные символы, находящиеся в начале или в конце строки. К таким символам, в частности, относятся пробелы и знаки табуляции. Удалены будут и символы конца строки.
Выглядит это так.
const string = `
First
Second`.trim()
▍Интерполяция
Под интерполяцией здесь понимается преобразование переменных и выражений в строки. Делается это с использованием конструкции ${}
.
const variable = 'test'
const string = `something ${ variable }` //something test
В блок ${}
можно добавлять всё что угодно — даже выражения.
const string = `something ${1 + 2 + 3}`
const string2 = `something ${foo() ? 'x' : 'y' }`
В константу string
попадёт текст something 6
, в константу string2
будет записан либо текст something x
, либо текст something y
. Это зависит от того, истинное или ложное значение вернёт функция foo()
(здесь применяется тернарный оператор, который, если то, что находится до знака вопроса, является истинным, возвращает то, что идёт после знака вопроса, в противном случае возвращая то, что идёт после двоеточия).
▍Тегированные шаблоны
Тегированные шаблоны применяются во множестве популярных библиотек. Среди них — Styled Components, Apollo, GraphQL.
То, что выводят такие шаблоны, подчиняется некоей логике, задаваемой с помощью функции. Вот немного переработанный пример, приведённый в одной из наших публикаций, иллюстрирующий работу с тегированными шаблонными строками.
const esth = 8
function helper(strs, ...keys) {
const str1 = strs[0] //ES
const str2 = strs[1] //is
let additionalPart = ''
if (keys[0] == 8) { //8
additionalPart = 'awesome'
}
else {
additionalPart = 'good'
}
return `${str1}${keys[0]}${str2}${additionalPart}.`
}
const es = helper`ES ${esth} is `
console.log(es) //ES 8 is awesome.
Здесь, если в константе esth
записано число 8
, в es
попадёт строка ES 8 is awesome
. В противном случае там окажется другая строка. Например, если в esth
будет число 6
, то она будет выглядеть как ES 6 is good
.
В Styled Components тегированные шаблоны используются для определения CSS-строк.
const Button = styled.button`
font-size: 1.5em;
background-color: black;
color: white;
`;
В Apollo они применяются для определения GraphQL-запросов.
const query = gql`
query {
...
}
`
Зная то, как устроены тегированные шаблоны, несложно понять, что styled.button
и gql
из предыдущих примеров — это просто функции.
function gql(literals, ...expressions) {
}
Например, функция gql()
возвращает строку, которая может быть результатом любых вычислений. Параметр literals
этой функции представляет собой массив, содержащий разбитое на части содержимое шаблонного литерала, expresions
содержит результаты вычисления выражений.
Разберём следующую строку.
const string = helper`something ${1 + 2 + 3} `
В функцию helper
попадёт массив literals
, содержащий два элемента. В первом будет текст something
с пробелом после него, во втором — пустая строка — то есть то, что находится между выражением ${1 + 2 + 3}
и концом строки. В массиве espressions
будет один элемент — 6
.
Вот более сложный пример.
const string = helper`something
another ${'x'}
new line ${1 + 2 + 3}
test`
Здесь в функцию helper
, в качестве первого параметра попадёт следующий массив.
[ 'something\nanother ', '\nnew line ', '\ntest' ]
Второй массив будет выглядеть так.
[ 'x', 6 ]
Итоги
Сегодня мы поговорили об обработке исключений, об автоподстановке точки с запятой и о шаблонных литералах в JavaScript. В следующий раз мы рассмотрим ещё некоторые важные концепции языка. В частности — работу в строгом режиме, таймеры, математические вычисления.
Уважаемые читатели! Пользуетесь ли вы возможностями тегированных шаблонов в JavaScript?