Руководство по JavaScript, часть 9: обзор возможностей стандартов ES7, ES8 и ES9 +40


Сегодня, в девятой части перевода руководства по JavaScript, будет сделан обзор возможностей, которые появились в языке благодаря стандартам ES7, ES8 и ES9.

> Часть 1: первая программа, особенности языка, стандарты
> Часть 2: стиль кода и структура программ
> Часть 3: переменные, типы данных, выражения, объекты
> Часть 4: функции
> Часть 5: массивы и циклы
> Часть 6: исключения, точка с запятой, шаблонные литералы
> Часть 7: строгий режим, ключевое слово this, события, модули, математические вычисления
> Часть 8: обзор возможностей стандарта ES6
> Часть 9: обзор возможностей стандартов ES7, ES8 и ES9



Стандарт ES7


Стандарт ES7, который, в соответствии с официальной терминологией, называется ES2016, вышел летом 2016 года. Он, в сравнении с ES6, принёс в язык не так много нового. В частности, речь идёт о следующем:

  • Метод Array.prototype.includes().
  • Оператор возведения в степень.

?Метод Array.prototype.includes()


Метод Array.prototype.includes() предназначен для проверки наличия в массиве некоего элемента. Находя в массиве искомое, он возвращает true, не находя — false. До ES7 для выполнения той же операции служил метод indexOf(), который возвращает, в случае нахождения элемента, первый индекс, по которому его можно обнаружить в массиве. Если же indexOf() элемента не находит — он возвращает число -1.

В соответствии с правилами преобразования типов JavaScript число -1 преобразуется в true. Как результат, для проверки результатов работы indexOf() следовало пользоваться не особенно удобной конструкцией следующего вида.

if ([1,2].indexOf(3) === -1) {
  console.log('Not found')
}

Если в подобной ситуации, полагая, что indexOf(), не находя элемента, возвращает false, воспользоваться чем-то вроде показанного ниже, код будет работать неправильно.

if (![1,2].indexOf(3)) { //неправильно
  console.log('Not found')
}

В данном случае оказывается, что конструкция ![1,2].indexOf(3) даёт false.

С использованием метода includes() подобные сравнения выглядят гораздо логичнее.

if (![1,2].includes(3)) {
  console.log('Not found')
}

В данном случае конструкция [1,2].includes(3) возвращает false, это значение оператор ! превращает в true и в консоль попадает сообщение о том, что искомый элемент в массиве не найден.

?Оператор возведения в степень


Оператор возведения в степень выполняет ту же функцию, что и метод Math.pow(), но пользоваться им удобнее, чем библиотечной функцией, так как он является частью языка.

Math.pow(4, 2) == 4 ** 2 //true

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

Стандарт ES8


Стандарта ES8 (ES2017) вышел в 2017 году. Он, как и ES7, внёс в язык не особенно много нового. А именно, речь идёт о следующих возможностях:

  • Дополнение строк до заданной длины.
  • Метод Object.values().
  • Метод Object.entries().
  • Метод Object.getOwnPropertyDescriptors().
  • Завершающие запятые в параметрах функций.
  • Асинхронные функции.
  • Работа с разделяемой памятью и атомарные операции.

?Дополнение строк до заданной длины


В ES8 появились два новых метода объекта StringpadStart() и padEnd().

Метод padStart() заполняет текущую строку другой строкой до тех пор, пока итоговая строка не достигнет нужной длины. Заполнение происходит в начале строки (слева). Вот как пользоваться этим методом.

str.padStart(targetLength [, padString])

Здесь str — это текущая строка, targetLength — длина итоговой строки (если она меньше длины текущей строки — эта строка будет возвращена без изменений), padString — необязательный параметр — строка, используемая для заполнения текущей строки. Если параметр padString не задан — для дополнения текущей строки до заданной длины используется символ пробела.

Метод padEnd() аналогичен padStart(), но заполнение строки происходит справа.

Рассмотрим примеры использования этих методов.

const str = 'test'.padStart(10)
const str1 = 'test'.padEnd(10,'*')

console.log(`'${str}'`) //'      test'
console.log(`'${str1}'`) //'test******'

Здесь, при использовании padStart() с указанием лишь желаемой длины итоговой строки, в начало исходной строки были добавлены пробелы. При использовании padEnd() с указанием длины итоговой строки и строки для её заполнения в конец исходной строки были добавлены символы *.

?Метод Object.values()


Этот метод возвращает массив, содержащий значения собственных свойств объекта, то есть таких свойств, которые содержит сам объект, а не тех, которые доступны ему через цепочку прототипов.

Вот как им пользоваться.

const person = { name: 'Fred', age: 87 }
const personValues = Object.values(person) 
console.log(personValues) // ['Fred', 87]

Этот метод применим и к массивам.

?Метод Object.entries()


Этот метод возвращает массив, каждый элемент которого также является массивом, содержащим, в формате [key, value], ключи и значения собственных свойств объекта.

const person = { name: 'Fred', age: 87 }
const personValues = Object.entries(person) 
console.log(personValues) // [['name', 'Fred'], ['age', 87]]

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

?Метод getOwnPropertyDescriptors()


Этот метод возвращает сведения обо всех собственных свойствах объекта. Со свойствами объектов ассоциированы наборы атрибутов (дескрипторы). В частности, речь идёт о следующих атрибутах:

  • value — значение свойства объекта.
  • writable — содержит true если свойство можно менять.
  • get — содержит функцию-геттер, связанную со свойством, или, если такой функции нет — undefined.
  • set — содержит функцию-сеттер для свойства или undefined.
  • configurable — если тут будет false — свойство нельзя удалять, нельзя менять его атрибуты за исключением значения.
  • enumerable — если в этом свойстве будет содержаться true — свойство является перечислимым.

Вот как пользоваться этим методом.

Object.getOwnPropertyDescriptors(obj)

Он принимает объект, сведения о свойствах которого нужно узнать, и возвращает объект, содержащий эти сведения.

const person = { name: 'Fred', age: 87 }
const propDescr = Object.getOwnPropertyDescriptors(person)
console.log(propDescr) 
/*
{ name:
   { value: 'Fred',
     writable: true,
     enumerable: true,
     configurable: true },
  age:
   { value: 87,
     writable: true,
     enumerable: true,
     configurable: true } }
*/

Зачем нужен этот метод? Дело в том, что он позволяет создавать мелкие копии объектов, копируя, помимо других свойств, геттеры и сеттеры. Этого нельзя было сделать, пользуясь для копирования объектов методом Object.assign(), который появился в стандарте ES6.

В следующем примере имеется объект с сеттером, который выводит, с помощью console.log() то, что пытаются записать в его соответствующее свойство.

const person1 = {
  set name(newName) {
      console.log(newName)
  }
}

person1.name = 'x' // x

Попробуем скопировать этот объект, воспользовавшись методом assign().

const person2 = {}
Object.assign(person2, person1)

person2.name = 'x' // в консоль ничего не попадает, сеттер не скопирован

Как видно, такой подход не работает. Свойство name, которое в исходном объекте было сеттером, теперь представлено в виде обычного свойства.

Теперь выполним копирование объекта с использованием методов Object.defineProperties() (он появился в ES5.1) и Object.getOwnPropertyDescriptors().

const person3 = {}
Object.defineProperties(person3,
  Object.getOwnPropertyDescriptors(person1))

person3.name = 'x' //x

Здесь в копии объекта сеттер остался.

Надо отметить, что ограничения, характерные для Object.assign(), свойственны и для метода Object.create() при использовании его для клонирования объектов.

?Завершающие запятые в параметрах функций


Эта возможность позволяет оставлять запятую в конце списка параметров или аргументов, соответственно, при объявлении и при вызове функций.

const doSomething = (
  var1, 
  var2,
) => {
  //...
}
doSomething(
  'test1',
  'test2',
)

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

?Асинхронные функции


В стандарте ES2017 появилась конструкция async/await, которую можно считать важнейшим новшеством этой версии языка.

Асинхронные функции представляют собой комбинацию промисов и генераторов, они упрощают конструкции, для описания которых раньше требовался большой объём шаблонного кода и неудобные в работе цепочки промисов. Фактически, речь идёт о высокоуровневой абстракции над промисами.

Когда в стандарте ES2015 появились промисы, они призваны были решить существующие проблемы с асинхронным кодом, что они и сделали. Но за те два года, которые разделяют стандарты ES2015 и ES2017, стало ясно, что промисы нельзя считать окончательным решением этих проблем.

В частности, промисы были нацелены на решение проблемы «ада коллбэков», но, решив эту проблему, они сами показали себя не с лучшей стороны из-за усложнения кода, в котором они используются. Собственно говоря, конструкция async/await решает проблему промисов и повышает удобство работы с асинхронным кодом.

Рассмотрим пример.

function doSomethingAsync() {
  return new Promise((resolve) => {
      setTimeout(() => resolve('I did something'), 3000)
  })
}
async function doSomething() {
  console.log(await doSomethingAsync())
}
console.log('Before')
doSomething()
console.log('After')

Этот код выведет в консоль следующее.

Before
After
I did something

Как видно, после вызова doSomething() программа продолжает выполняться, после Before в консоль тут же выводится After, а после того, как пройдут три секунды, выводится I did something.

Последовательный вызов асинхронных функций


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

function promiseToDoSomething() {
  return new Promise((resolve)=>{
      setTimeout(() => resolve('I did something'), 10000)
  })
}
async function watchOverSomeoneDoingSomething() {
  const something = await promiseToDoSomething()
  return something + ' and I watched'
}
async function watchOverSomeoneWatchingSomeoneDoingSomething() {
  const something = await watchOverSomeoneDoingSomething()
  return something + ' and I watched as well'
}
watchOverSomeoneWatchingSomeoneDoingSomething().then((res) => {
  console.log(res) // I did something and I watched and I watched as well
})

?Разделяемая память и атомарные операции


Здесь речь идёт об объекте SharedArrayBuffer, который позволяет описывать разделяемые области памяти, и об объекте Atomics, который содержит набор атомарных операций в виде статических методов. Подробности о возможностях, которые дают программисту эти объекты, можно почитать здесь.

Стандарт ES9


ES9 (ES2018) — это самая свежая на момент публикации данного материала версия стандарта. Вот её основные возможности:

  • Применение операторов spread и rest к объектам.
  • Асинхронные итераторы.
  • Метод Promise.prototype.finally().
  • Улучшения регулярных выражений.

?Применение операторов spread и rest к объектам


Мы уже говорили об операторах rest и spread, которые появились в ES6 и могут быть использованы для работы с массивами. Оба они выглядят как три точки. Оператор rest, в следующем примере деструктурирования массива, позволяет поместить его первый и второй элементы в константы first и second, а все остальные — в константу others.

const numbers = [1, 2, 3, 4, 5]
const [first, second, ...others] = numbers
console.log(first) //1
console.log(second) //2
console.log(others) //[ 3, 4, 5 ]

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

const numbers = [1, 2, 3, 4, 5]
const sum = (a, b, c, d, e) => a + b + c + d + e
const res = sum(...numbers)
console.log(res) //15

Теперь, используя тот же подход, можно работать и с объектами. Вот пример использования оператора rest в операции деструктурирующего присваивания.

const { first, second, ...others } = 
  { first: 1, second: 2, third: 3, fourth: 4, fifth: 5 }
console.log(first) //1
console.log(second) //2
console.log(others) //{ third: 3, fourth: 4, fifth: 5 }

Вот оператор spread, применяемый при создании нового объекта на основе существующего. Этот пример продолжает предыдущий.

const items = { first, second, ...others }
console.log(items) //{ first: 1, second: 2, third: 3, fourth: 4, fifth: 5 }

?Асинхронные итераторы


Новая конструкция for-await-of позволяет вызывать асинхронные функции, возвращающие промисы, в циклах. Такие циклы ожидают разрешения промиса перед переходом к следующему шагу. Вот как это выглядит.

for await (const line of readLines(filePath)) {
  console.log(line)
}

При этом надо отметить, что подобные циклы нужно использовать в асинхронных функциях — так же, как это делается при работе с конструкцией async/await.

?Метод Promise.prototype.finally()


Если промис успешно разрешается — осуществляется вызов очередного метода then(). Если что-то идёт не так — вызывается метод catch(). Метод finally() позволяет выполнять некий код независимо от того, что происходило до этого.

fetch('file.json')
  .then(data => data.json())
  .catch(error => console.error(error))
  .finally(() => console.log('finished'))

?Улучшения регулярных выражений


В регулярных выражениях появилась возможность ретроспективной проверки строк (?<=). Это позволяет искать в строках некие конструкции, перед которыми есть какие-то другие конструкции.

Возможность опережающих проверок, использующая конструкцию ?=, имелась в регулярных выражениях, реализованных в JavaScript, и до стандарта ES2018. Такие проверки позволяют узнать, следует ли за неким фрагментом строки другой фрагмент.

const r = /Roger(?= Waters)/
const res1 = r.test('Roger is my dog')
const res2 = r.test('Roger is my dog and Roger Waters is a famous musician')
console.log(res1) //false
console.log(res2) //true

Конструкция ?! выполняет обратную операцию — совпадение будет найдено только в том случае, если за заданной строкой не идёт другая строка.

const r = /Roger(?! Waters)/g
const res1 = r.test('Roger is my dog')
const res2 = r.test('Roger is my dog and Roger Waters is a famous musician')
console.log(res1) //true
console.log(res2) //false

При ретроспективной проверке, как уже было сказано, используется конструкция ?<=.

const r = /(?<=Roger) Waters/
const res1 = r.test('Pink Waters is my dog')
const res2 = r.test('Roger is my dog and Roger Waters is a famous musician')
console.log(res1) //false
console.log(res2) //true

Операцию, обратную описанной, можно выполнить с помощью конструкции ?<!.

const r = /(?<!Roger) Waters/
const res1 = r.test('Pink Waters is my dog')
const res2 = r.test('Roger is my dog and Roger Waters is a famous musician')
console.log(res1) //true
console.log(res2) //false

Управляющие последовательности Unicode в регулярных выражениях


В регулярных выражениях можно использовать класс \d, соответствующий любой цифре, класс \s, соответствующий любому пробельному символу, класс \w, который соответствует любому буквенно-цифровому символу, и так далее. Возможность, о которой идёт речь, расширяет набор классов, которыми можно пользоваться в регулярных выражениях, позволяя работать с Unicode-последовательностями. Речь идёт о классе \p{} и об обратном ему классе \P{}.

В Unicode каждый символ имеет набор свойств. Эти свойства указываются в фигурных скобках группы \p{}. Так, например, свойство Script определяет семейство языков, к которому принадлежит символ, свойство ASCII, логическое, принимает значение true для ASCII-символов, и так далее. Например, выясним, содержат ли некие строки исключительно ASCII-символы.

console.log(r.test('abc')) //true
console.log(r.test('ABC@')) //true
console.log(r.test('ABCЖ')) //false

Свойство ASCII_Hex_Digit принимает значение true только для символов, которые можно использовать для записи шестнадцатеричных чисел.

const r = /^\p{ASCII_Hex_Digit}+$/u

console.log(r.test('0123456789ABCDEF')) //true
console.log(r.test('H')) //false

Существует и множество других подобных свойств, которые используются так же, как вышеописанные. Среди них — Uppercase, Lowercase, White_Space, Alphabetic, Emoji.

Вот, например, как с помощью свойства Script определить, какой алфавит используется в строке. Здесь мы проверяем строку на использование греческого алфавита.

const r = /^\p{Script=Greek}+$/u
console.log(r.test('????????')) //true
console.log(r.test('hey')) //false

Подробности об этих свойствах можно почитать здесь.

Именованные группы


Захваченным группам символов в ES2018 можно давать имена. Вот как это выглядит.

const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
const result = re.exec('2015-01-02')
console.log(result)
/*
[ '2015-01-02',
  '2015',
  '01',
  '02',
  index: 0,
  input: '2015-01-02',
  groups: { year: '2015', month: '01', day: '02' } ]
*/

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

const re = /(\d{4})-(\d{2})-(\d{2})/
const result = re.exec('2015-01-02')
console.log(result) 
/*
[ '2015-01-02',
  '2015',
  '01',
  '02',
  index: 0,
  input: '2015-01-02',
  groups: undefined ]
*/

Флаг регулярных выражений s


Использование флага s приводит к тому, что символ . (точка) будет, кроме прочих, соответствовать и символу новой строки. Без использования этого флага точка соответствует любому символу за исключением символа новой строки.

console.log(/hi.welcome/.test('hi\nwelcome')) // false
console.log(/hi.welcome/s.test('hi\nwelcome')) // true

Итоги


Этим материалом мы завершаем публикацию переводов данного руководства по JavaScript. Надеемся, эти публикации помогли тем, кто раньше с JavaScript не работал, сделать их первые шаги в программировании на этом языке.

Уважаемые читатели! Если вы раньше не писали на JS и осваивали этот язык по данному руководству — просим поделиться впечатлениями.




К сожалению, не доступен сервер mySQL