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

Работа с модулями

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

Динамический импорт

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

import * as MyModule from "./my-module.js";

У этого оператора есть пара ограничений:

  • весь код импортированного модуля оценивается во время загрузки текущего модуля
  • спецификатор модуля ("./my-module.js" в приведенном выше примере) является строковой константой, и вы не можете изменить ее во время выполнения

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

Новый оператор import() решает эти проблемы, позволяя динамически импортировать модули. Оператор принимает спецификатор модуля в качестве аргумента и возвращает обещание. Кроме того, спецификатором модуля может быть любое выражение, возвращающее строку. Это отличная новость, потому что теперь мы можем загружать модули JavaScript во время выполнения, как в следующем примере:

const baseModulePath = "./modules";
const btnBooks = document.getElementById("btnBooks");
let bookList = [];
btnBooks.addEventListener("click", async e => {
  const bookModule = await import(`${baseModulePath}/books.js`);
  bookList = bookModule.loadList();
});

Этот код показывает, как загрузить модуль books.js сразу, когда пользователь нажимает кнопку btnBooks. После загрузки модуля обработчик события щелчка будет использовать функцию loadList(), экспортированную модулем. Обратите внимание на то, как импортируемый модуль указывается с помощью строковой интерполяции.

Долгожданный динамический импорт теперь доступен в JS 2020.

Импортировать метаданные

Объект import.meta предоставляет метаданные для текущего модуля. Механизм JavaScript создает его, и его текущее доступное свойство - url. Значением этого свойства является URL-адрес, с которого был загружен модуль, включая любой параметр запроса или хэш.

Например, вы можете использовать свойство import.meta.url для создания URL-адреса файла data.json, хранящегося в той же папке текущего модуля. Следующий код дает такой результат:

const dataUrl = new URL("data.json", import.meta.url);

В этом случае import.meta.url предоставляет классу URL базовый URL для файла data.json.

Новый синтаксис экспорта

Оператор import, введенный спецификациями ECMAScript 2015, предоставляет вам множество форм импорта модулей. Вот несколько примеров:

import {value} from "./my-module.js";
import * from "./my-module.js";

В некоторых случаях может потребоваться экспортировать объекты, импортированные из другого модуля. Вам может помочь удобный синтаксис export, как показано ниже:

export {value} from "./my-module.js";
export * from "./my-module.js";

Эта симметрия между операторами импорта и экспорта удобна с точки зрения опыта разработчика. Однако до этих новых спецификаций не поддерживался конкретный случай:

import * as MyModule from "./my-module.js";

Чтобы экспортировать пространство имен MyModule, вы должны использовать два оператора:

import * as MyModule from "./my-module.js";
export {MyModule};

Теперь вы можете получить тот же результат с помощью одного оператора, как показано ниже:

export * as MyModule from "./my-module.js";

Это дополнение упрощает ваш код и сохраняет симметрию между операторами import и export.

Типы данных и объекты

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

BigInt и целые числа произвольной точности

Как вы знаете, в JavaScript есть только один тип данных для чисел: тип Number. Этот примитивный тип позволяет вам представлять 64-битные числа с плавающей запятой. Конечно, он также представляет собой целые числа, но максимальное представимое значение - 253, что соответствует константе Number.MAX_SAFE_INTEGER.

Не вдаваясь во внутренние детали целочисленного представления, есть ситуации, когда вам может потребоваться более высокая точность. Рассмотрим следующие случаи:

  • взаимодействие с другими системами, которые предоставляют данные в виде 64-битных целых чисел, таких как GUID, номера счетов или идентификаторы объектов
  • результат сложных математических вычислений, требующих более 64 бит

Обходной путь для первого случая - представление данных в виде строк. Конечно, во втором случае этот обходной путь не работает.

Новый тип данных BigInt призван решить эти проблемы. Вы представляете литерал BigInt, просто добавляя букву n к числу, как показано в этом примере:

const aBigInteger = 98765432123456789n;

Вы также можете использовать конструктор BigInt() так же, как конструктор Number():

const aBigInteger = BigInt("98765432123456789");

Оператор typeof теперь возвращает строку "bigint" при применении к значению BigInt:

typeof aBigInteger      //output: "bigint"

Имейте в виду, что Number и BigInt относятся к разным типам, поэтому их нельзя смешивать. Например, попытка добавить значение Number к значению BigInt вызывает исключение TypeError, как показано на следующем рисунке:

Вы должны явно преобразовать значение Number в значение BigInt с помощью конструктора BigInt().

Метод matchAll () для регулярных выражений

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

const regExp = /page (\d+)/g;
const text = 'text page 1 text text page 2';
let matches;
while ((matches = regExp.exec(text)) !== null) {
  console.log(matches);
}

Этот код сопоставляет все экземпляры page x в переменной text через итерацию. На каждой итерации метод exec() обрабатывает входную строку, и вы получаете следующий результат:

["page 1", "1", index: 5, input: "text page 1 text text page 2", groups: undefined]
["page 2", "2", index: 22, input: "text page 1 text text page 2", groups: undefined]

Метод matchAll() объектов String позволяет получить тот же результат, но более компактным способом и с лучшей производительностью. В следующем примере предыдущий код переписывается с использованием этого нового метода:

const regExp = /page (\d+)/g;
const text = 'text page 1 text text page 2';
let matches = [...text.matchAll(regExp)];
for (const match of matches) {
  console.log(match);
}

Метод matchAll() возвращает итератор. В предыдущем примере оператор распространения используется для сбора результата итератора в массив.

Объект globalThis

Для доступа к глобальному объекту требуется различный синтаксис в зависимости от среды JavaScript. Например, в браузере глобальным объектом является window, но вы не можете использовать его в Web Worker. В этом случае вам нужно использовать self. Кроме того, в Node.js глобальным объектом является global.

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

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

Метод Promise.allSettled ()

В настоящее время в JavaScript есть два способа комбинировать обещания: Promise.all() и Promise.race().

Оба метода принимают в качестве аргумента массив обещаний. Ниже приводится пример использования Promise.all():

const promises = [fetch("/users"), fetch("/roles")];
const allResults = await Promise.all(promises);

Promise.all() возвращает обещание, которое выполняется, когда выполняются все обещания. Если хотя бы одно обещание отклонено, возвращенное обещание отклоняется. Причина отклонения полученного обещания такая же, как и для первого отклоненного обещания.

Такое поведение не дает вам прямого способа получить результат всех обещаний, если хотя бы одно из них отклонено. Например, в приведенном выше коде, если fetch("/users") терпит неудачу и соответствующее обещание отклонено, у вас нет простого способа узнать, выполнено ли обещание fetch("/roles") или отклонено. Чтобы получить эту информацию, вам нужно написать дополнительный код.

Новый комбинатор Promise.allSettled() ожидает выполнения всех обещаний, независимо от их результата. Итак, следующий код позволяет узнать результат каждого обещания:

const promises = [fetch("/users"), fetch("/roles")];
const allResults = await Promise.allSettled(promises);
const errors = results
  .filter(p => p.status === 'rejected')
  .map(p => p.reason);

Еще один пример,

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

Новые операторы

Пара новых операторов упростит написание и чтение кода в очень распространенных операциях. Угадай, какие?

Оператор объединения с нулевым значением

Сколько раз вы видели и использовали такие выражения?

const size = settings.size || 42;

Оператор || очень часто используется для присвоения значения по умолчанию, когда вы пытаетесь назначить значение null или undefined. Однако такой подход может привести к некоторым потенциально непредвиденным результатам.

Например, константе size в приведенном выше примере будет присвоено значение 42 также, когда значение settings.size равно 0. Но значение по умолчанию также будет присвоено, если значение settings.size равно "" или false.

Чтобы преодолеть эти потенциальные проблемы, теперь вы можете использовать нулевой оператор объединения (??). Предыдущий код выглядит следующим образом:

const size = settings.size ?? 42;

Это означает, что значение по умолчанию 42 будет присвоено константе size, только если значение settings.size равно null или undefined.

Необязательная цепочка

Рассмотрим следующий пример:

const txtName = document.getElementById("txtName");
const name = txtName ? txtName.value : undefined;

Вы получаете текстовое поле с txtName в качестве идентификатора из текущего HTML-документа. Однако, если элемент HTML не существует в документе, константа txtName будет null. Итак, прежде чем обращаться к его свойству value, вы должны убедиться, что txtName не null или undefined.

Необязательный оператор цепочки (?.) позволяет получить более компактный и читаемый код, как показано ниже:

const txtName = document.getElementById("txtName");
const name = txtName?.value;

Как и в предыдущем примере, константа name будет иметь значение txtName.value, если txtName не равно null или undefined; undefined иначе.

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

const customerCity = invoice?.customer?.address?.city;

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

const userName = user?.["name"];

Кроме того, это относится и к вызовам функций или методов:

const fullName = user.getFullName?.();

В этом случае, если метод getFullName() существует, он выполняется. В противном случае выражение возвращает undefined.

«Нулевой оператор объединения и необязательная цепочка теперь стали реальностью в JavaScript».

Использование новых функций

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

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

Резюме

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

Какая ваша любимая особенность ES2020? Расскажите мне об этом, написав в Твиттере и связавшись со мной в Twitter и Instagram!