Только ES2021 (или ES12) был зарелижен этим летом, как многие члены сообщества уже начали заглядывать в будущее, размышляя, в частности, о том, какие новые фичи принесет нам ES2022.

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

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

Объявления полей класса

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

class Post{
    title;
    content;
    shares = 0;
}

Приватные методы и поля

Благодаря этому новому способу определения полей класса теперь также можно определять приватные поля — посредством префикса #, как показано в примере ниже:

class User {
    name;
    #password;
}

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

class User {
    name;
    #password;
    get #password(){
        return #password;
    }
    #clear(){
        this.name = null;
        this.#password = null;
    }
}

Статические публичные методы и поля

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

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

class Environment {
    name;
    port;
    static hostname = 'localhost';
    static get hostname(){
        return hostname;
    }
}

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

Индексы совпадений регулярных выражений

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

Следующий фрагмент кода показывает, как работает это предложение:

const regexp = /test(\d)/g; //without the /d flag
const regexp2022 = /test(\d)/dg; //with the /d flag
const str = 'test1test2';
const array = [...str.matchAll(regexp)];
const array2022 = [...str.matchAll(regexp2022)];
console.log(array[0]);
console.log(array2022[0]);
console.log(array[1]);
console.log(array2022[1]);

Верхнеуровневый await  

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

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

import {getUser} from './data/user'

let user = await getUser();
//

Благодаря этому новому предложению код выше будет работать без проблем, а в старой спецификации он будет выводить SyntaxError: await is only valid in async function

Эргономичный brand-checking для приватных полей

Когда вы попытаетесь получить доступ к публичному полю, которое не объявлено, вы попросту получите undefined, при этом доступ к приватным полям вызывает исключение. Исходя из этого, мы можем проверить, имеет ли класс приватное поле, проверив, генерируется ли исключение при попытке доступа к нему. Но это предложение предоставляет нам более элегантное решение, заключающееся в использовании оператора in, который возвращает true, если указанное свойство/поле находится в указанном объекте/классе, заставляя его работать с приватными полями, как вы можете видеть в следующем примере кода:

class User {
    name;
    #password;
    get #password(){
        return #password;
    }
    #clear(){
        this.name = null;
        this.#password = null;
    }
    static hasPassword(obj){
        return #password in obj;
    }
}

Новый метод .at() для всех встроенных индексируемых сущностей

Это предложение представляет собой новый метод Array, позволяющий получить элемент по заданному индексу. Когда мы подставляем в этот метод положительный индекс, он ведет себя так же, как и стандартный доступ через скобки, но когда мы подставляем отрицательный целочисленный индекс, он работает как “отрицательная индексация” в Python, т.е. будет производится отсчет в обратном порядке от последнего элемента массива. Таким образом, поведение метода в таком виде array.at(-1) будет аналогично array[array.length-1], что можно увидеть в следующем примере:

const array = [0,1,2,3,4,5];

console.log(array[array.length-1]); // 5
console.log(array.at(-1)); //5
//то же поведение

console.log(array[array.lenght-2]); // 4
console.log(array.at(-2)); //4
//то же поведение

Доступный Object.prototype.hasOwnProperty()

Иногда Object.prototype может быть недоступен или переопределен. Например, Object.create(null) создаст объект, который не наследуется от Object.prototype, что сделает его методы недоступными. Кроме того, вы не можете быть уверены, что вызов .hasOwnProperty() действительно вызывает встроенный метод, потому что он может быть перезаписан, если вы не владеете напрямую каждым свойством объекта.

Чтобы избежать этих проблем, для вызова hasOwnProperty() обычно используется метод call(), как показано в примере ниже:

const obj = { foo:'bar' }
let hasFoo = Object.prototype.hasOwnProperty.call(obj, 'foo');
console.log(hasFoo); //true

Это предложение добавляет метод Object.hasOwn(object, property), который ведет себя также, как и вызов Object.prototype.hasOwnProperty.call(object, property). Этот новый метод hasOwn(object, property) предоставляет нам доступный способ проверки свойств объекта, более удобный, чем предыдущие решения, как вы можете видеть ниже:

const obj = { foo:'bar' }
let hasFoo = Object.hasOwn(obj, 'foo');
console.log(hasFoo); //true

Блоки статической инициализации классов ECMAScript

Это предложение даем нам элегантный способ вычисления блоков статической инициализации кода во время объявления/определения класса с доступом к его приватным полям.

Текущие предложения касательно статических и статических приватных полей предоставляют механизм для выполнения инициализации статической части класса по каждому полю во время ClassDefinitionEvaluation, однако есть некоторые случаи, которые реализовать так легко. Например, если вам нужно вычислить операторы во время инициализации (например, try..catch) или установить два поля из одного значения, вы должны выполнить эту логику вне определения класса.

Это можно понять на следующем примере:

Без статических блоков:

class User {
    static roles;
    name;
    #password;
}

try { 
    User.roles = getRolesFromDb();
} catch { 
    User.roles = getRolesFromBackup();
}

Со статическими блоками:

class User {
    static roles;
    name;
    #password;
    static {
        try { 
            User.roles = getRolesFromDb();
        } catch { 
            User.roles = getRolesFromBackup();
        }
    }
}

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

На этом все, если у вас есть какие-либо сомнения или предложения по этому поводу, пожалуйста, дайте мне знать.

А также, привет!

Ссылки и дополнительная информация:


Материал подготовлен в рамках специализации "Fullstack Developer"

Всех желающих приглашаем на бесплатное demo-занятие «Такие разные числа!». На занятии будут рассмотрены такие типы данных в JavaScript, как number и bigint, а также особенности их устройства и операций с ними.
>> РЕГИСТРАЦИЯ

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


  1. ionicman
    14.12.2021 09:43
    +4

    Странно, почему нельзя придерживаться единого стиля наименования?
    Приватное поле "#", статическое «static».

    Ну либо уж дальше одним символом обозначать и static тоже, либо словом «private» приватное поле (что с моей тз лучше и читабельней).


    1. mayorovp
      14.12.2021 11:21

      Потому что приватные поля находятся в отдельном пространстве имён, в отличии от статических. Свойство foo и #foo — это два разных свойства, а не одно с двумя областями видимости.


      Напомню, что любое свойство foo можно прочитать через квадратные скобки: obj['foo']. А вот с приватными свойствами такое не работает.


      Мне вот больше интересно почему метод hasOwn попал в Object, а не в Reflect...


      1. ionicman
        14.12.2021 16:21
        +1

        Речь была про обозначение — токен «private» vs "#".
        Причем тут область видимости / пространство имен?


        1. mayorovp
          14.12.2021 18:49

          Предположим, сделано как вы предлагаете. Вот вы пишете: obj.foo. Как интерпретатор должен понять, вы к публичному свойству foo обратились или к приватному?


          1. ionicman
            14.12.2021 23:49

            По внутреннему описателю свойства «foo» во внутренней объектной структуре.
            Что было объявлено первым, то и есть, иначе кидается исключение.

            Грубо говоря при парсинге исходника у свойства «foo» взводится флаг, что оно — приватное (или статическое и т.д.) и переопределить это нельзя. Область должна быть одна — в одном объекте (или цепочке объектов наследования) свойство с одинаковым именем, но разными областями видимости — это стрельба себе в ногу.


            1. mayorovp
              15.12.2021 01:20

              Тогда сломается прозрачность при наследовании.


              class Foo { #name="foo"; printFoo() { console.log(this.#name); } }
              class Bar extends Foo { #name="bar"; printBar() { console.log(this.#name); } }
              
              const obj = new Bar();
              obj.printFoo(); // foo
              obj.printBar(); // bar

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


              Кстати, вот ещё прикол:


              class Foo { name = "foo"; }
              class Bar extends Foo { #name = "bar"; }
              
              const obj = new Bar();
              console.log(obj.name); // foo

              А как у вас смогут одновременно существовать унаследованное публичное и приватное свойство с одним именем?


              1. ionicman
                15.12.2021 11:28

                При вызове метода из объекта, метод оперирует скопом данного объекта же?

                Т.е. если private name в классе FOO, и метод printFoo в классе FOO, то при вызове его из отнаследованого класса, скоп будет FOO, а не BAR (перегрузки не было) => выдаст «foo».

                А при вызове printBar мы обращаемся к объекту BAR, соответственно скоп будет его, и он выведет его private name => выдаст «bar».

                А как у вас смогут одновременно существовать унаследованное публичное и приватное свойство с одним именем
                Нет, это даст экспешн, ибо как я уже писал выше — это стрельба себе в ногу.
                Тип свойства задается в самом нижнем классе, где оно было объявлено и не может быть переопределен.


                1. mayorovp
                  15.12.2021 12:05

                  Нет, это даст экспешн, ибо как я уже писал выше — это стрельба себе в ногу.

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


                  При вызове метода из объекта, метод оперирует скопом данного объекта же?

                  А как тогда вот этот пример разрулить?


                  class Foo {
                      #name = "foo";
                  
                      print1(obj) {
                          console.log(obj.name);
                      }
                  
                      print2(obj) {
                          console.log(obj.#name);
                      }
                  }

                  Как вы выразите отличие между методами print1 и print2?


                  1. ionicman
                    15.12.2021 12:17
                    -1

                    Представьте, что базовый класс находится в библиотеке. С вашим подходом любое добавление свойства потенциально может оказаться ломающим изменением.
                    Каким образом? Если свойство в библиотеке объявлено как «private», то при любом наследовании от нее, оно и останется «private», что где ломается? Или вы имеете ввиду, что где-то вверху, при наследовании, кто-то объявил такое-же свойство с другим типом? Ну в таком случае сразу будет понятно где ошибка по эксепшену и это нормально. Ну и при наследовании от библиотек, да и в принципе при наследовании от того, что может меняться независимо, есть хороший тон — использовать префиксы перед переменными, чтобы избежать конфликтов. Потому как гораздо более печально будет, если перетрется значение переменной в верхнем классе и при этом вы не получите ни какого предупреждения.

                    Как вы выразите отличие между методами print1 и print2?
                    obj — это откуда? можно пополней пример?

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

                    Если, например, private name для объекта o, и вы попробуете сделать o.name = "..." => получите ошибку, так как оно — приватное, если попробуете отнаследовать класс от o, у которого это свойство будет другого типа (например было обычным, хотите чтобы стало приватным) => также получите ошибку. Т.е. для объекта не может быть свойства с одинаковым именем и разным типом на любом уровне наследования, включая сам объект — т.е. случай, где у объекта есть обычное свойство name и приватное свойство name физически невозможен, только один из вариантов.

                    Вобщем все тоже самое что в ООП для C++.


                    1. mayorovp
                      15.12.2021 13:09

                      Каким образом?

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


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

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


                      obj — это откуда? можно пополней пример?

                      Ну держите полнее:


                      class Foo {
                          #name = "foo";
                          print1(obj) { console.log(obj.name); }
                          print2(obj) { console.log(obj.#name); }
                      }
                      
                      const obj = new Foo();
                      obj.print1({ name: "bar" });
                      obj.print2(obj);

                      Вобщем все тоже самое что в ООП для C++.

                      Совсем не то же самое. На плюсах типы статические, что всё упрощает. И приватные члены из родительского класса полностью невидимы в дочернем.


    1. shornikov
      14.12.2021 14:00

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