Руководство по JavaScript, часть 8: обзор возможностей стандарта ES6

в 9:00, , рубрики: javascript, Блог компании RUVDS.com, обучение, разработка, Разработка веб-сайтов

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

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

Руководство по JavaScript, часть 8: обзор возможностей стандарта ES6 - 1

О стандарте ES6

Стандарт ES6, который правильнее было бы называть ES2015 или ECMAScript 2015 (это — его официальные наименования, хотя все называют его ES6), появился через 4 года после выхода предыдущего стандарта — ES5.1. На разработку всего того, что вошло в стандарт ES5.1, ушло около десяти лет. В наши дни всё то, что появилось в этом стандарте, превратилось в привычные инструменты JS-разработчика. Надо отметить, что ES6 внёс в язык серьёзнейшие изменения (сохраняя обратную совместимость с его предыдущими версиями). Для того чтобы оценить масштаб этих изменений, можно отметить, что размер документа, описывающего стандарт ES5, составляет примерно 250 страниц, а стандарт ES6 описывается в документе, состоящем уже из приблизительно 600 страниц.

В перечень наиболее важных новшеств стандарта ES2015 можно включить следующие:

  • Стрелочные функции
  • Промисы
  • Генераторы
  • Ключевые слова let и const
  • Классы
  • Модули
  • Поддержка шаблонных литералов
  • Поддержка параметров функций, задаваемых по умолчанию
  • Оператор spread
  • Деструктурирующее присваивание
  • Расширение возможностей объектных литералов
  • Цикл for...of
  • Поддержка структур данных Map и Set

Рассмотрим эти возможности.

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

Стрелочные функции изменили внешний вид и особенности работы JavaScript-кода. С точки зрения внешнего вида их использование делает объявления функций короче и проще. Вот объявление обычной функции.

const foo = function foo() {
  //...
}

А вот практически такая же (хотя и не полностью аналогичная вышеобъявленной) стрелочная функция.

const foo = () => {
  //...
}

Если тело стрелочной функции состоит лишь из одной строки, результат выполнения которой нужно из этой функции вернуть, то записывается она ещё короче.

const foo = () => doSomething()

Если стрелочная функция принимает лишь один параметр, записать её можно следующим образом.

const foo = param => doSomething(param)

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

Особенности ключевого слова this в стрелочных функциях

У стрелочных функций нет собственного значения this, они наследуют его из контекста выполнения.

Это устраняет проблему, для решения которой при использовании обычных функций приходилось, для сохранения контекста, использовать конструкции наподобие var that = this. Однако, как было показано в предыдущих частях руководства, это изменение серьёзно сказывается на особенностях работы со стрелочными функциями и на сфере их применения.

Промисы

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

JavaScript-разработчики использовали промисы и до появления стандарта ES2015, применяя для этого различные библиотеки (например — jQuery, q, deferred.js, vow). Это говорит о важности и востребованности данного механизма. Разные библиотеки реализуют его по-разному, появление стандарта в этой области можно считать весьма позитивным фактом.
Вот код, написанный с использованием функций обратного вызова (коллбэков).

setTimeout(function() {
  console.log('I promised to run after 1s')
  setTimeout(function() {
    console.log('I promised to run after 2s')
  }, 1000)
}, 1000)

С использованием промисов это можно переписать следующим образом.

const wait = () => new Promise((resolve, reject) => {
  setTimeout(resolve, 1000)
})
wait().then(() => {
  console.log('I promised to run after 1s')
  return wait()
})
.then(() => console.log('I promised to run after 2s'))

Генераторы

Генераторы — это особые функции, которые могут приостанавливать собственное выполнение и возобновлять его. Это позволяет, пока генератор находится в состоянии ожидания, выполняться другому коду.

Генератор самостоятельно принимает решение о том, что ему нужно приостановиться и позволить другому коду, «ожидающему» своей очереди, выполниться. При этом у генератора есть возможность продолжить своё выполнение после того, как та операция, результатов выполнения которой он ждёт, окажется выполненной.

Всё это делается благодаря единственному простому ключевому слову yield. Когда в генераторе встречается это ключевое слово — его выполнение приостанавливается.
Генератор может содержать множество строк с этим ключевым словом, приостанавливая собственное выполнение несколько раз. Генераторы объявляют с использованием конструкции *function. Эту звёздочку перед словом function не стоит принимать за нечто вроде оператора разыменования указателя, применяемого в языках наподобие C, C++ или Go.

Генераторы знаменуют своим появлением новую парадигму программирования на JavaScript. В частности, они дают возможность двустороннего обмена данными между генератором и другим кодом, позволяют создавать долгоживущие циклы while, которые не «подвешивают» программу.

Рассмотрим пример, иллюстрирующий особенности работы генераторов. Вот сам генератор.

function *calculator(input) {
    var doubleThat = 2 * (yield (input / 2))
    var another = yield (doubleThat)
    return (input * doubleThat * another)
}

Такой командой мы его инициализируем.

const calc = calculator(10)

Затем мы обращаемся к его итератору.

calc.next()

Эта команда запускает итератор, она возвращает такой объект.

{
  done: false
  value: 5
}

Здесь происходит следующее. В коде выполняется функция, использующая значение input, переданное конструктору генератора. Код генератора выполняется до тех пор, пока в нём не встретится ключевое слово yield. В этот момент он возвращает результат деления input на 2, что, так как input равняется 10, даёт число 5. Это число мы получаем благодаря итератору, и, вместе с ним, указание на то, что работа генератора пока не завершена (свойство done в объекте, возвращённом итератором, установлено в значение false), то есть, функция пока лишь приостановлена.
При следующем вызове итератора мы передаём в генератор число 7.

calc.next(7)

В ответ на это итератор возвращает нам следующий объект.

{
  done: false
  value: 14
}

Здесь число 7 было использовано при вычислении значения doubleThat.

На первый взгляд может показаться, что код input / 2 представляет собой нечто вроде аргумента некоей функции, но это — лишь значение, возвращаемое на первой итерации. Здесь мы это значение пропускаем и используем новое входное значение 7, умножая его на 2. После этого мы доходим до второго ключевого слова yield, в результате значение, полученное на второй итерации, равняется 14.

На следующей итерации, которая является последней, мы передаём в генератор число 100.

calc.next(100)

В ответ получаем следующий объект.

{
  done: true
  value: 14000
}

Итерация завершена (в генераторе больше не встречается ключевое слово yield), в объекте возвращается результат вычисления выражения (input * doubleThat * another), то есть — 10 * 14 * 100 и указание на завершение работы итератора (done: true).

Ключевые слова let и const

В JavaScript для объявления переменных всегда использовалось ключевое слово var. Такие переменные имеют функциональную область видимости. Ключевые слова let и const позволяют, соответственно, объявлять переменные и константы, обладающие блочной областью видимости.

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

Ключевое слово const работает так же как и let, но с его помощью объявляют константы, которые являются иммутабельными.

В современном JS-коде ключевое слово var используется редко. Оно уступило место ключевым словам let и const. При этом, что может показаться необычным, ключевое слово const используется сегодня весьма широко, что говорит о популярности идей иммутабельности сущностей в современном программировании.

Классы

Сложилось так, что JavaScript был единственным чрезвычайно широко распространённым языком, использующим модель прототипного наследования. Программисты, переходящие на JS с языков, реализующих механизм наследования, основанный на классах, чувствовали себя в такой среде неуютно. Стандарт ES2015 ввёл в JavaScript поддержку классов. Это, по сути, «синтаксический сахар» вокруг внутренних механизмов JS, использующих прототипы. Однако это влияет на то, как именно пишут JS-приложения.

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

class Person {
  constructor(name) {
    this.name = name
  }
  hello() {
    return 'Hello, I am ' + this.name + '.'
  }
}
class Actor extends Person {
  hello() {
    return super.hello() + ' I am an actor.'
  }
}
var tomCruise = new Actor('Tom Cruise')
console.log(tomCruise.hello()) 

Эта программа выводит в консоль текст Hello, I am Tom Cruise. I am an actor.
В JS-классах нельзя объявлять переменные экземпляров, их нужно инициализировать в конструкторах.

▍Конструктор класса

У классов есть специальный метод, constructor, который вызывается при создании экземпляра класса с использованием ключевого слова new.

▍Ключевое слово super

Ключевое слово super позволяет обращаться к родительскому классу из классов-потомков.

▍Геттеры и сеттеры

Геттер для свойства можно задать следующим образом.

class Person {
  get fullName() {
    return `${this.firstName} ${this.lastName}`
  }
}

Сеттер можно описать так, как показано ниже.

class Person {
  set age(years) {
    this.theAge = years
  }
}

С геттерами и сеттерами работают так, как будто они представляют собой не функции, а обычные свойства объектов.

Модули

До появления стандарта ES2015 существовало несколько конкурирующих подходов к работе с модулями. В частности, речь идёт о технологиях RequireJS и CommonJS. Такая ситуация приводила к разногласиям в сообществе JS-разработчиков.

В наши дни, благодаря стандартизации модулей в ES2015, ситуация постепенно нормализуется.

▍Импорт модулей

Модули импортируют с использованием конструкции вида import...from.... Вот несколько примеров.

import * as something from 'mymodule'
import React from 'react'
import { React, Component } from 'react'
import React as MyLibrary from 'react'

▍Экспорт модулей

Внутренние механизмы модуля закрыты от внешнего мира, но из модуля можно экспортировать всё то, что он может предложить другим модулям. Делается это с помощью ключевого слова export.

export var foo = 2
export function bar() { /* ... */ }

▍Шаблонные литералы

Шаблонные литералы представляют собой новый способ описания строк в JavaScript. Вот как это выглядит.

const aString = `A string`

Кроме того, использование синтаксиса шаблонных литералов позволяет внедрять в строки выражения, интерполировать их. Делается это с помощью конструкции вида ${a_variable}. Вот простой пример её использования:

const v = 'test'
const str = `something ${v}` //something test

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

const str = `something ${1 + 2 + 3}`
const str2 = `something ${foo() ? 'x' : 'y' }`

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

const str3 = `Hey
this
string
is awesome!`

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

var str = 'Onen' +
'Twon' +
'Three'

Параметры функций, задаваемые по умолчанию

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

const foo = function(index = 0, testing = true) { /* ... */ }
foo()

Оператор spread

Оператор spread (оператор расширения) позволяет «раскрывать» массивы, объекты или строки. Этот оператор выглядит как три точки (...). Сначала рассмотрим его на примере массива.

const a = [1, 2, 3]

Вот как на основании этого массива создать новый массив.

const b = [...a, 4, 5, 6]

Вот как создать копию массива.

const c = [...a]

Этот оператор работает и с объектами. Например — вот как с его помощью можно клонировать объект.

const newObj = { ...oldObj }

Применив оператор spread к строке, можно преобразовать её в массив, в каждом элементе которого содержится один символ из этой строки.

const hey = 'hey'
const arrayized = [...hey] // ['h', 'e', 'y']

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

const f = (foo, bar) => {}
const a = [1, 2]
f(...a)

Раньше это делалось с использованием конструкции вида f.apply(null, a), но такой код и писать сложнее, и читается он хуже.

Деструктурирующее присваивание

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

const person = {
  firstName: 'Tom',
  lastName: 'Cruise',
  actor: true,
  age: 54,
}
const {firstName: name, age} = person

Здесь из объекта извлекаются свойства firstName и age. Свойство age записывается в объявляемую тут же константу с таким же именем, а свойство firstName, после извлечения, попадает в константу name.

Деструктурирующее присваивание подходит и для работы с массивами.

const a = [1,2,3,4,5]
const [first, second, , , fifth] = a

В константы first, second и fifth попадут, соответственно, первый, второй и пятый элементы массива.

Расширение возможностей объектных литералов

В ES2015 значительно расширены возможности описания объектов с помощью объектных литералов.

▍Упрощение включения в объекты переменных

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

const something = 'y'
const x = {
  something: something
}

Теперь то же самое можно сделать так.

const something = 'y'
const x = {
  something
}

▍Прототипы

Прототип объекта теперь можно задать с помощью следующей конструкции.

const anObject = { y: 'y' }
const x = {
  __proto__: anObject
}

▍Ключевое слово super

С использованием ключевого слова super объекты могут обращаться к объектам-прототипам. Например — для вызова их методов, имеющих такие же имена, как методы самих этих объектов.

const anObject = { y: 'y', test: () => 'zoo' }
const x = {
  __proto__: anObject,
  test() {
    return super.test() + 'x'
  }
}
x.test() //zoox

▍Вычисляемые имена свойств

Вычисляемые имена свойств формируются на этапе создания объекта.

const x = {
  ['a' + '_' + 'b']: 'z'
}
x.a_b //z

Цикл for...of

В 2009 году, в стандарте ES5, появились циклы forEach(). Это — полезная конструкция, к минусам которой относится тот факт, что такие циклы очень неудобно прерывать. Классический цикл for в ситуациях, когда выполнение цикла нужно прервать до его обычного завершения, оказывается гораздо более адекватным выбором.

В ES2015 появился цикл for...of, который, с одной стороны, отличается краткостью синтаксиса и удобством forEach, а с другой — поддерживает возможности по досрочному выходу из цикла.

Вот пара примеров цикла for...of.

//перебор значений элементов массива
for (const v of ['a', 'b', 'c']) {
  console.log(v);
}
//перебор значений элементов массива с выводом их индексов благодаря использованию метода entries()
for (const [i, v] of ['a', 'b', 'c'].entries()) {
  console.log(i, v);
}

Структуры данных Map и Set

В ES2015 появились структуры данных Map и Set (а также их «слабые» варианты WeakMap и WeakSet, использование которых позволяет улучшить работу «сборщика мусора» — механизма, ответственного за управление памятью в JS-движках). Это — весьма популярные структуры данных, которые, до появлениях их официальной реализации, приходилось имитировать имеющимися средствами языка.

Итоги

Сегодня мы сделали обзор возможностей стандарта ES2015, которые чрезвычайно сильно повлияли на современное состояние языка. Нашей следующей темой будут особенности стандартов ES2016, ES2017 и ES2018.

Уважаемые читатели! Какие новшества стандарта ES6 кажутся вам наиболее полезными?

Автор: ru_vds

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js