Если вы пишете JavaScript сегодня, стоит потратить время на то, чтобы быть в курсе всех обновлений языка за последние несколько лет. С 2015 года, с выпуском ES6, каждый год выпускается новая версия спецификации ECMAScript. Каждая итерация добавляет в язык новые функции, новый синтаксис и улучшения качества жизни. Механизмы JavaScript в большинстве браузеров и Node.js быстро догоняют, и будет справедливо, если ваш код тоже догонит. Это потому, что с каждой новой итерацией JavaScript появляются новые идиомы и новые способы выражения вашего кода, и во многих случаях эти изменения могут сделать код более удобным для вас и ваших сотрудников.

Вот некоторые из последних функций ECMAScript, а также JavaScript и Node.js, которые вы можете использовать, чтобы писать более чистый, лаконичный и читаемый код.

1. Заблокируйте объявления с оценкой

С момента создания языка разработчики JavaScript использовали var для объявления переменных. У ключевого слова var есть свои причуды, самая проблемная из которых - это область действия переменных, созданных с его помощью.

var x = 10
if (true) {
  var x = 15     // inner declaration overrides declaration in parent scope
  console.log(x) // prints 15
}
console.log(x)   // prints 15

Поскольку переменные, определенные с помощью var, не имеют блочной области видимости, их переопределение в более узкой области влияет на значение внешней области.

Теперь у нас есть два новых ключевых слова, которые заменяют var, а именно let и const, которые не страдают этим недостатком.

let y = 10
if (true) {
  let y = 15       // inner declaration is scoped within the if block
  console.log(y)   // prints 15
}
console.log(y)     // prints 10

const и let отличаются семантикой: переменные, объявленные с помощью const, не могут быть переназначены в их области действия. Это не означает, что они неизменяемы, только то, что их ссылки не могут быть изменены.

const x = []
x.push("Hello", "World!")
x // ["Hello", "World!"]
x = [] // TypeError: Attempted to assign to readonly property.

2. Стрелочные функции

Стрелочные функции - еще одна очень важная функция, недавно появившаяся в JavaScript. У них много преимуществ. Прежде всего, они делают функциональные аспекты JavaScript красивыми и удобными для написания.

let x = [1, 2, 3, 4]
x.map(val => val * 2)                // [2, 4, 6, 8]
x.filter(val => val % 2 == 0)        // [2, 4]
x.reduce((acc, val) => acc + val, 0) // 10

Во всех приведенных выше примерах стрелочные функции, названные в честь характерной стрелки =>, заменяют традиционные функции кратким синтаксисом.

  1. Если тело функции представляет собой одно выражение, подразумеваются скобки области {} и return, и их не нужно записывать.
  2. Если функция имеет единственный аргумент, подразумеваются круглые скобки () аргументов, и их не нужно записывать.
  3. Если выражение тела функции является словарем, оно должно быть заключено в круглые скобки ().

Еще одно важное преимущество стрелочных функций состоит в том, что они не определяют область видимости, а скорее существуют в родительской области. Это позволяет избежать множества ошибок, которые могут возникнуть при использовании ключевого слова this. Стрелочные функции не привязаны к this. Внутри стрелочной функции значение this такое же, как и в родительской области. Следовательно, стрелочные функции нельзя использовать в качестве методов или конструкторов. Стрелочные функции не работают с apply, bind или call и не имеют привязки к super.

У них также есть некоторые другие ограничения, такие как отсутствие объекта arguments, к которому могут обращаться традиционные функции, и невозможность yield из тела функции.

Таким образом, стрелочные функции не являются заменой 1: 1 стандартным функциям, а являются желанным дополнением к набору функций JavaScript.

3. Необязательное связывание

Представьте себе глубоко вложенную структуру данных, подобную этому объекту person здесь. Предположим, вы хотели получить доступ к имени и фамилии этого человека. Вы бы написали это на JavaScript так:

person = {
  name: {
    first: 'John',
    last: 'Doe',
  },
  age: 42
}
person.name.first // 'John'
person.name.last  // 'Doe'

Теперь представьте, что произошло бы, если бы объект person не содержал вложенного объекта name.

person = {
  age: 42
}
person.name.first // TypeError: Cannot read property 'first' of undefined
person.name.last  // TypeError: Cannot read property 'last' of undefined

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

person && person.name && person.name.first // undefined

Встречайте опциональную цепочку, новую функцию JavaScript, которая устраняет это чудовище. Необязательная цепочка прерывает процесс копания, как только он встречает значение null или undefined, и возвращает undefined, не вызывая ошибки.

person?.name?.first // undefined

Результирующий код намного лаконичнее и чище.

4. Нулевое слияние

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

function print(val) {
	return val || 'Missing'
}
print(undefined) // 'Missing'
print(null)      // 'Missing'
print(0)         // 'Missing'
print('')        // 'Missing'
print(false)     // 'Missing'
print(NaN)       // 'Missing'

В JavaScript теперь предлагается нулевой оператор объединения ??, который предлагает лучшую альтернативу, поскольку он приводит к откату, только если предыдущее выражение имеет нулевое значение. Здесь нулевой символ относится к значениям null или undefined.

function print(val) {
	return val ?? 'Missing'
}
print(undefined) // 'Missing'
print(null)      // 'Missing'
print(0)         // 0
print('')        // ''
print(false)     // false
print(NaN)       // NaN

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

5. Логическое присвоение

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

if (x === null || x == undefined) {
	x = y
}

Если вы знали, как работает короткое замыкание, вы могли бы заменить эти 3 строки кода более сжатой версией, используя нулевой оператор объединения.

x ?? (x = y) // x = y if x is nullish, else no effect

Здесь мы используем функцию короткого замыкания оператора объединения с нулевым значением, чтобы выполнить вторую часть x = y, если x имеет значение NULL. Код довольно лаконичен, но его все еще не очень легко читать или понимать. Логическое присвоение нулевого значения устраняет необходимость в таком обходном пути.

x ??= y // x = y if x is nullish, else no effect

В том же духе JavaScript также вводит операторы логического присваивания И &&= и логического присваивания ИЛИ ||=. Эти операторы выполняют присваивание только при выполнении определенного условия и не имеют никакого эффекта в противном случае.

x ||= y // x = y if x is falsy, else no effect
x &&= y // x = y if x is truthy, else no effect

Совет от профессионала: если вы писали ранее на Ruby, вы видели операторы ||= и &&=, поскольку в Ruby нет концепции ложных значений.

6. Именованные группы захвата.

Начнем с краткого обзора групп захвата в регулярных выражениях. Группа захвата - это часть строки, которая соответствует части регулярного выражения в круглых скобках.

let re = /(\d{4})-(\d{2})-(\d{2})/
let result = re.exec('Pi day this year falls on 2021-03-14!')
result[0] // '2020-03-14', the complete match
result[1] // '2020', the first capture group
result[2] // '03', the second capture group
result[3] // '14', the third capture group

Регулярные выражения также уже довольно давно поддерживают именованные группы захвата, что позволяет ссылаться на группы захвата по имени, а не по индексу. Теперь, с ES9, эта функция перешла в JavaScript. Теперь объект результата содержит объект вложенных групп, в котором значение каждой группы захвата сопоставляется с ее именем.

let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
let result = re.exec('Pi day this year falls on 2021-03-14!')
result.groups.year  // '2020', the group named 'year'
result.groups.month // '03', the group named 'month'
result.groups.day   // '14', the group named 'day'

Новый API прекрасно работает с другой новой функцией JavaScript - деструктурированными назначениями.

let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
let result = re.exec('Pi day this year falls on 2021-03-14!')
let { year, month, day } = result.groups
year  // '2020'
month // '03'
day   // '14'

7. async & await

Один из мощных аспектов JavaScript - его асинхронность. Это означает, что многие функции, которые могут выполняться долго или отнимать много времени, могут возвращать Promise, а не блокировать выполнение.

const url = 'https://the-one-api.dev/v2/book'
let prom = fetch(url)
prom // Promise {<pending>}
// wait a bit
prom // Promise {<fullfilled>: Response}, if no errors
// or
prom // Promise {<rejected>: Error message}, if any error

Вызов fetch возвращает обещание, которое при создании имеет статус «ожидающий». Вскоре, когда API возвращает ответ, он переходит в состояние «выполнено», и отклик, который он обертывает, становится доступным. В мире обещаний вы бы сделали что-то подобное, чтобы вызвать API и проанализировать ответ как JSON.

const url = 'https://the-one-api.dev/v2/book'
let prom = fetch(url)
prom                               // Promise {<fullfilled>: Response}
  .then(res => res.json())
  .then(json => console.log(json)) // prints response, if no errors
  .catch(err => console.log(err))  // prints error message, if any error

В 2017 году JavaScript объявил о двух новых ключевых словах async и await, которые упрощают работу с обещаниями и работу с ними. Они не заменяют обещания; они просто синтаксический сахар поверх мощных концепций Promises.

Вместо того, чтобы весь код происходил внутри серии функций «затем», await делает все это похожим на синхронный JavaScript. В качестве дополнительного преимущества вы можете использовать try...catch с await вместо обработки ошибок в функциях «catch», как если бы вы использовали Promises напрямую. Тот же код с await будет выглядеть так.

const url = 'https://the-one-api.dev/v2/book'
let res = await fetch(url) // Promise {<fullfilled>: Response} -await-> Response
try {
	let json = await res.json()
	console.log(json) // prints response, if no errors
} catch(err) {
  console.log(err)  // prints error message, if any error
}

Ключевое слово async - это другая сторона той же медали, поскольку оно обертывает любые данные, которые должны быть отправлены в Promise. Рассмотрим следующую асинхронную функцию для сложения нескольких чисел. В реальном мире ваш код делал бы что-то гораздо более сложное.

async function sum(...nums) {
    return nums.reduce((agg, val) => agg + val, 0)
}
sum(1, 2, 3)                    // Promise {<fulfilled>: 6}
  .then(res => console.log(res) // prints 6
let res = await sum(1, 2, 3)    // Promise {<fulfilled>: 6} -await-> 6
console.log(res)                // prints 6

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

Первоначально опубликовано в Блоге DeepSource.