Стандарт ECMAScript 2021 (12 редакция) выпущен 22 июня 2021 года. Вместе с ним появились новые возможности и новые синтаксические конструкции. Все эти улучшения направлены на то, чтобы сделать JavaScript надёжнее и стабильнее, чтобы разработчикам легче было делать своё дело.



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

1. Разделители разрядов чисел


Разделители разрядов чисел позволяют записывать числовые литералы с использованием символов подчёркивания, что улучшает читабельность чисел. Система автоматически удаляет эти символы при подготовке кода к запуску. В следующем коде демонстрируется пример использования разделителей.

// Десятичное число, разряды которого разделены символом подчёркивания.
let n1 = 1_000_000_000;
console.log(n1); // Выведет: 1000000000

// Десятичное число, разряды которого разделены символом подчёркивания.
let n2 = 1_000_000_000.150_200
console.log(n2); // Выведет: 1000000000.1502

// Шестнадцатеричное число, байты которого разделены символом подчёркивания.
let n3 = 0x95_65_98_FA_A9
console.log(n3); // Выведет: 641654651561

// BigInt-число, разряды которого разделены символом подчёркивания.
let n4 = 155_326_458_156_248_168_514n
console.log(n4); // This will print: 155326458156248168514n

2. Метод String.prototype.replaceAll()


Новый метод строковых объектов replaceAll() позволяет заменить на что либо все вхождения подстроки в строке. При использовании строкового метода replace() осуществляется лишь замена первого вхождения искомого значения. А метод replaceAll() позволяет заменить все вхождения такого значения. Вот пример:

// Объявляем константу, хранящую некое строковое значение.
const orgStr = 'JavaScript, often abbreviated as JS, is a programming language that conforms to the ECMAScript specification. JavaScript is high-level, often just-in-time compiled and multi-paradigm.';

// Для того чтобы заменить в исходной строке лишь первое вхождение искомой подстроки и сформировать новую строку - воспользуемся методом replace().
let newStr = orgStr.replace('JavaScript', 'TypeScript');
console.log(newStr);

// Для замены всех вхождений искомой строки воспользуемся методом replaceAll().
let newStr2 = orgStr.replaceAll('JavaScript', 'TypeScript');
console.log(newStr2);

3. Метод Promise.any() и объект AggregateError


Метод Promise.any() можно назвать полной противоположностью метода Promise.all(). Promise.any(), при передаче ему итерируемого объекта с промисами, возвращает промис со значением первого успешно выполненного промиса. Если ни один из промисов не разрешён — метод возвращает сведения об ошибках, в частности — объект AggregateError. А Promise.all() ждёт разрешения всех промисов. Есть и другие методы объекта Promise. Например — Promise.allSettled(), который ожидает успешного завершения или отклонения всех переданных ему промисов.

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

// Создаём объект Promise.
const promise1 = new Promise((resolve, reject) => {
    // Разрешить промис через 2 секунды.
    setTimeout(() => resolve("The first promise has been resolved."), 2000);
});

// Создаём объект Promise.
const promise2 = new Promise((resolve, reject) => {
    // Разрешить промис через 1 секунду.
    setTimeout(() => resolve("The second promise has been resolved."), 1000);
});

// Создаём объект Promise.
const promise3 = new Promise((resolve, reject) => {
    // Разрешить промис через 3 секунды.
    setTimeout(() => resolve("The third promise has been resolved."), 3000);
});

(async function () {
    const data = await Promise.any([promise1, promise2, promise3]);
    // Выводим данные, возвращённые из первого разрешённого промиса.
    console.log(data);
    // Предыдущая команда выведет следующее: The second promise has been resolved.
})()

Если все промисы были отклонены — будет возвращён объект AggregateError, содержащий сведения об ошибках. Обратитесь к следующему примеру для того чтобы разобраться с тем, как обрабатывать подобные ошибки:

// Создаём объект Promise.
const promise1 = new Promise((resolve, reject) => {
    // Отклонить промис через 1 секунду.
    setTimeout(() => reject("The first promise has been rejected."), 1000);
});

// Создаём объект Promise.
const promise2 = new Promise((resolve, reject) => {
    // Отклонить промис через 500 миллисекунд.
    setTimeout(() => reject("The second promise has been rejected."), 500);
});

// Попытаемся выполнить промисы.
(async function () {
    try {
        const data = await Promise.any([promise1, promise2]);
        console.log(data);
    } catch (error) {
        // Если все промисы отклонены, мы попадём в этот блок try-catch, где 
        // можно будет обработать ошибки, с которыми завершилась работа промисов.
        console.log("Error: ", error);
    }
})();

4. Логические операторы присваивания


В ECMAScript 2021 появились три новых оператора, которые представляют собой комбинацию оператора присваивания и логических операторов.

  • Логический оператор присваивания ИЛИ (OR): ||=.
  • Логический оператор присваивания И (AND): &&=.
  • Логический оператор присваивания с проверкой значений на null и undefined: ??=.

▍4.1. Оператор ||=


Оператор ||= принимает два операнда и назначает правый операнд левому только в том случае, если левый операнд является ложным. Вот пример:

// Оператор ||= проверит, не является ли ложным (0) значение songsCount.
// Если это так, правый операнд будет записан в переменную, находящуюся в левой части выражения.
let myPlaylist = {songsCount: 0, songs:[]};
myPlaylist.songsCount ||= 100;
console.log(myPlaylist); // Выведет: {songsCount: 100, songs: Array(0)}

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

a || (a = b)

▍4.2. Оператор &&=


Оператор &&= присваивает правый операнд левому только в том случае, если значение, находящееся слева, является истинным. Рассмотрим пример:

// Оператор &&= проверит, является ли значение filesCount истинным.
// Если это так - правый операнд будет присвоен левому.
let myFiles = {filesCount: 100, files:[]};
myFiles.filesCount &&= 5;
console.log(myFiles); // Выведет: {filesCount: 5, files: Array(0)}

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

a && (a = b)

▍4.3. Оператор ??=


Оператор ??= присвоит правый операнд левому только в том случае, если левый оператор представлен значением null или undefined. Например:

// Оператор ??= проверит, равняется ли lastname null или undefined.
// Если это так - правый операнд будет присвоен левому.
let userDetails = {firstname: 'Katina', age: 24}
userDetails.lastname ??= 'Dawson';
console.log(userDetails); // Выведет: {firstname: 'Katina', age: 24, lastname: 'Dawson'}

5. Приватные методы экземпляров классов и методы доступа к свойствам


Свойства и методы экземпляров классов в JavaScript, по умолчанию, общедоступны. Но теперь можно создавать приватные методы и свойства с использованием префикса #. Доступны они только изнутри экземпляра класса. Вот как пользоваться приватными методами:

// Создадим класс User.
class User {
    constructor() {}

    // Приватный метод можно создать, поместив '#' перед
    // именем метода.
    #generateAPIKey() {
        return "d8cf946093107898cb64963ab34be6b7e22662179a8ea48ca5603f8216748767";
    }

    getAPIKey() {
        // Обратиться к приватному методу можно, поставив '#' перед
        // именем метода.
        return this.#generateAPIKey();
    }
}

const user = new User();
const userAPIKey = user.getAPIKey();
console.log(userAPIKey); // Выведет: d8cf946093107898cb64963ab34be6b7e22662179a8ea48ca5603f8216748767

Приватные методы доступа к свойствам объектов — это приватные геттеры и сеттеры. Геттер позволяет получить значение свойства, а сеттер позволяет назначить свойству значение.

Объявить приватный геттер можно так:

get #newAccountPassword() {}

Так можно объявить приватный сеттер:

set #generateAccountPassword(newPassword) {}

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

// Создадим класс Str.
class Str {
    // Приватные свойства можно создавать, помещая '#'
    // перед их именами.
    #uniqueStr;

    constructor() {}

    // Приватный сеттер можно создать, поместив '#' перед
    // его именем.
    set #generateUniqueStringByCustomLength(length = 24) {
        const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
        let randomStr = "";

        for (let i = 0; i < length; i++) {
            const randomNum = Math.floor(Math.random() * characters.length);
            randomStr += characters[randomNum];
        }

        this.#uniqueStr = randomStr;
    }

    // Общедоступный сеттер
    set setRandomString(length) {
        this.#generateUniqueStringByCustomLength = length;
    }

    // Приватный геттер можно создать, поместив '#' перед
    // его именем.
    get #fetchUniqueString() {
        return this.#uniqueStr;
    }

    // Общедоступный геттер
    get getRandomString() {
        return this.#fetchUniqueString;
    }
}

const str = new Str();
// Вызываем общедоступный сеттер, который обратится к приватному сеттеру,
// находящемуся в том же объекте, что и он.
str.setRandomString = 20;

// Вызываем общедоступный геттер, который обратится к приватному геттеру,
// находящемуся в том же объекте, что и он.
const uniqueStr = str.getRandomString;
console.log(uniqueStr); // Выводит случайную строку каждый раз, когда геттер вызывают после вызова сеттера

Итоги


Теперь вы знаете, как пользоваться некоторыми новыми возможностями JavaScript ES12 (ECMAScript 2021). А значит — пришло время применить эти знания на практике.

Пользуетесь ли вы возможностями JavaScript ES12 в своих проектах?

Комментарии (25)


  1. Bigata
    18.11.2021 20:48

    Интересно, что быстрее replace() с регуляркой или replaceAll()


    1. vanxant
      18.11.2021 23:20

      replaceAll тоже может принимать регулярку, тогда это просто /g

      Вот если искомая строка - строка, то replaceAll должно быть чуть быстрее, как минимум на этапе компиляции.


  1. strannik_k
    18.11.2021 22:45
    +8

    Мда, скоро код на JS будет выглядеть как обфусцированный)


    1. leon0399
      19.11.2021 03:28
      +3

      Особенно смущает решение использовать # в качестве маркера приватных методов. Лично я пишу не только на JS и для меня это все ещё остаётся маркером комментариев


      1. Format-X22
        19.11.2021 04:44

        Подсветка синтаксиса решит не всю, но большую часть вашей боли. А так - было бы на клавиатуре побольше разных других знаков - возможно был бы он, но, по сути, это последний такой не используемый. Можно конечно ещё дописать private, это валидно, хоть и многословно, но может сломать TypeScript - приватные методы оттуда не эквивалентны тем что добавили в JS т.к. у добавленных приватность на уровне объекта самого, а не доступа до поля, разное поведение. Ну и в целом, возможно, плохая идея многословности добавлять именно в сам JS. Но всё это так, сугубо мои личные мысли.


        1. Immortal_pony
          19.11.2021 07:22
          +4

          Но на клавиатуре уже есть символ, который помогает пометить приватные методы в языках, которые их не поддерживают и этот символ "_". Почему бы просто не использовать его?

          Также можно было бы добавить ключевое слово "private" и вообще не использовать никакие странные символы.


          1. movl
            19.11.2021 11:58

            Просто приватные поля, это прям приватные, единственное место где к ним можно обращаться, это в рамках класса. Символ нужен чтобы интерпретатор смог понять, что идет обращение к приватному полю. При этом обращение через obj['#private'] остается валидным, и не является обращением к приватному полю, что позволяет сохранить обратную совместимость.


          1. Format-X22
            19.11.2021 14:27
            +3

            Если использовать _, то тогда мы сломаем с ходу огромное количество кода т.к. это валидное имя сейчас и в легаси много кто вызывает приватные методы, помеченные так, хотя как бы не правильно, но раз можно физически - то делают если очень надо. Будет не очень весело если апдейт языка половину интернета сломает, думаю вы со мной согласитесь.


            1. Akuma
              22.11.2021 12:38

              Да и не в легаси :)

              Как минимум, для тестов иногда полезно. Всегда проще напрямую вызвать в тесте obj._private() с @ts-ignore чем писать какие-то обертки


  1. Pavel1114
    19.11.2021 07:17
    +1

    Не густо. В основном косметика, кроме промисов и приватных методов. В сравнении с es6 — минорное обновление. Но так и должно быть — js довольно зрелый язык.


  1. artem_ibragimov
    19.11.2021 09:23

    Непонятно, чем отличается Promise.any от Promise.race, лучше бы сравнили с ним, чем с Promise.all


    1. Rishquer
      19.11.2021 14:41
      +3

      any возвращает первый успешный, race – первый с любым результатом.


  1. dopusteam
    19.11.2021 09:27
    +2

    set #generateUniqueStringByCustomLength(length = 24) {

    this.#generateUniqueStringByCustomLength = length;

    Выглядит ужасно, когда вместо метода делают сеттер, который тут совсем ни к чему.


    1. pavelsc
      24.11.2021 02:04
      -1

      Меня ещё сеттеры начинающиеся с set и геттеры с get покоробили. Они в принципе предполагают отсутствие любого глагола в имени. Надо на собеседованиях А4 страничку подобного шлака подавать и маркер, точно полезнее всякого литкода будет. А лучше ноут с кодом и потом мержреквест глянуть через полчаса )


  1. Vladivo
    19.11.2021 09:38
    -3

    Ещё чуть-чуть, кажется, и нативные типы с дженериками подвезут. Во заживём!


  1. MightyRavendark
    19.11.2021 14:05
    +1

    Добавили относительно бесполезные разделители разрядов чисел, при этом на реально нужные числа фиксированных размерностей даже предложения вроде нет. Если JS начинает позиционировать себя как язык для полноценных приложений (PWA), то часто требуется, например, unsigned математика, или деление с отбрасыванием остатка . В нормальных языках это решается путем объявления числа как unsigned и присваивание результата деления в int-переменную. В JS-же приходится писать костыли. При этом, т.к. все числа в js, по сути float64 - то на сложных вычислениях вообще получается ужас, как по костылям, так и по производительности. В некоторых частях кода, я наблюдал, как программисты делали примерно такой код:

    let uint8 = new Uint8Array([originalNumberValue]);


    Т.е. для создания и работы с одним uint8 числом, программистам приходится создавать TypedArray! Ну, либо, мучаться с битовыми масками и Math-функциями.


    1. misha98857
      22.11.2021 11:26
      +2

      А чем стандартный тип BigInt не устраивает?


  1. JimDi
    22.11.2021 05:42

    что странно - у меня в примере с запуском промисов первый срабатывает сначала. ЧЯДНТ?


  1. Methos
    22.11.2021 10:49
    -3

    Пофиг, всё равно TS используем =)


    1. Chamie
      22.11.2021 13:22
      +1

      TS не добавляет рантайм функционала, если что. Только компайл-тайм проверки и подсказки в IDE.


    1. Methos
      25.11.2021 14:58

      За такой невинный коммент сливать карму?

      Какой в этом смысл?


  1. Denulis
    22.11.2021 11:26

    Спасибо, за интересную за статью!


  1. Akuma
    22.11.2021 12:40
    -2

    А можно было просто добавить TypeScript в JS не все было бы хорошо.


  1. Louter
    22.11.2021 23:05

    Ещё интересен WeakRef (странная дичь, конешн), но блин

    Стандарт ES: вот вам разделители, вот вам приватные свойства и методы, даже промисы апнули!
    Двойное сравнение (a < b < c): ну да, ну да, пошли мы нахрен


    1. Fodin
      23.11.2021 00:26
      +1

      Двойного сравнения не будет в таком виде, т.к. такой синтаксис сейчас вполне валиден, просто обозначает не двойное сравнение.