Материал, первую часть перевода которого мы сегодня публикуем, посвящён новым стандартным возможностям JavaScript, о которых шла речь на конференции Google I/O 2019. В частности, здесь мы поговорим о регулярных выражениях, о полях классов, о работе со строками.



Ретроспективные проверки в регулярных выражениях


Регулярные выражения (Regular Expression, сокращённо — RegEx или RegExp) — это мощная технология обработки строк, которая реализована во множестве языков программирования. Регулярные выражения оказываются очень кстати в тех случаях, когда нужно, например, выполнять поиск фрагментов строк по сложным шаблонам. До недавнего времени в JavaScript-реализации регулярных выражений имелось всё кроме ретроспективных проверок (lookbehind).

Для того чтобы разобраться с тем, что такое ретроспективная проверка, поговорим сначала об опережающих проверках (lookahead), которые уже поддерживаются в JavaScript.

> Вторая часть

?Опережающая проверка


Синтаксис опережающих проверок в регулярных выражениях позволяет выполнять поиск фрагментов строк, когда известно, что правее их находятся другие фрагменты. Например, при работе со строкой MangoJuice, VanillaShake, GrapeJuice можно воспользоваться синтаксисом положительной опережающей проверки для нахождения слов, сразу после которых идёт слово Juice. В нашем случае это — слова Mango и Grape.

Существует два вида опережающих проверок. Это — положительные опережающие проверки (positive lookahead) и отрицательные опережающие проверки (negative lookahead).

Положительная опережающая проверка


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

/[a-zA-Z]+(?=Juice)/

Этот шаблон позволяет выбирать слова, состоящие из строчных или прописных букв, после которых есть слово Juice. Не стоит путать структуры, описывающие опережающие и ретроспективные проверки, с группами (capture group). Хотя условия этих проверок и записываются в круглых скобках, система не выполняет их захвата. Давайте рассмотрим пример положительной опережающей проверки.

const testString = "MangoJuice, VanillaShake, GrapeJuice";
const testRegExp = /[a-zA-Z]+(?=Juice)/g;
const matches = testString.match( testRegExp );
console.log( matches ); // ["Mango", "Grape"]

Отрицательная опережающая проверка


Если рассмотреть, используя вышеприведённую строку, механизм действия отрицательных опережающих проверок, то окажется, что они позволяют находить слова, правее которых нет слова Juice. Синтаксис отрицательных опережающих проверок похож на синтаксис положительных проверок. Однако в нём имеется одна особенность, которая заключается в том, что символ = (равно) меняется на символ ! (восклицательный знак). Вот как это выглядит:

/[a-zA-Z]+(?!Juice)/

Это регулярное выражение позволит выбрать все слова, правее которых нет слова Juice. Но при применении такого шаблона выбранными окажутся все слова в строке (MangoJuice, VanillaShake, GrapeJuice). Дело в том, что, по мнению системы, ни одно слово здесь не завершается Juice. В результате для того, чтобы достичь желаемого результата, нужно уточнить регулярное выражение и переписать его так:

/(Mango|Vanilla|Grape)(?!Juice)/

Использование этого шаблона позволит выбрать слова Mango, или Vanilla, или Grape, после которых нет слова Juice. Вот пример:

const testString = "MangoJuice, VanillaShake, GrapeJuice";
const testRegExp = /(Mango|Vanilla|Grape)(?!Juice)/g;
const matches = testString.match( testRegExp );
console.log( matches ); // ["Vanilla"]

?Ретроспективная проверка


По аналогии с синтаксисом опережающих проверок, синтаксис ретроспективных проверок позволяет выбирать последовательности символов только в том случае, если левее этих последовательностей находится некий заданный шаблон. Например, при обработке строки FrozenBananas, DriedApples, FrozenFish мы можем воспользоваться положительной ретроспективной проверкой для того, чтобы найти слова, левее которых есть слово Frozen. В нашем случае этому условию соответствуют слова Bananas и Fish.

Существуют, как и в случае с опережающими проверками, положительные ретроспективные проверки (positive lookbehind) и отрицательные ретроспективные проверки (negative или negating lookbehind).

Положительная ретроспективная проверка


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

/(?<=Frozen)[a-zA-Z]+/

Здесь используется символ <, которого не было при описании опережающих проверок. Кроме того, условие в регулярном выражении расположено не справа от интересующего нас шаблона, а слева. Используя вышеописанный шаблон можно выбрать все слова, начинающиеся с Frozen. Рассмотрим пример:

const testString = "FrozenBananas, DriedApples, FrozenFish";
const testRegExp = /(?<=Frozen)[a-zA-Z]+/g;
const matches = testString.match( testRegExp );
console.log( matches ); // ["Bananas", "Fish"]

Отрицательная ретроспективная проверка


Механизм отрицательных ретроспективных проверок позволяет искать в строках шаблоны, левее которых нет заданного шаблона. Например, если нужно выбрать в строке FrozenBananas, DriedApples, FrozenFish слова, которые не начинаются с Frozen, можно попытаться использовать такое регулярное выражение:

/(?<!Frozen)[a-zA-Z]+/

Но, так как использование этой конструкции приведёт к выбору всех слов из строки, так как ни одно из них не начинается с Frozen, регулярное выражение нужно уточнить:

/(?<!Frozen)(Bananas|Apples|Fish)/

Вот пример:

const testString = "FrozenBananas, DriedApples, FrozenFish";
const testRegExp = /(?<!Frozen)(Bananas|Apples|Fish)/g;
const matches = testString.match( testRegExp );
console.log( matches ); // ["Apples"]

> Поддержка


В этом и в других подобных разделах будут приводиться сведения об этапе согласования описываемых возможностей JS в техническом комитете 39 (Technical Committee 39, TC39), который отвечает в ECMA International за поддержку спецификаций ECMAScript. В таких разделах будут приведены и данные о версиях Chrome и Node.js (а иногда и о версии Firefox), начиная с которых можно пользоваться соответствующими возможностями.


Поля классов


Поле класса — это новая синтаксическая конструкция, используемая для определения свойств экземпляров класса (объектов) за пределами конструктора класса. Существуют два типа полей классов: публичные поля (public class fields) и приватные поля (private class fields).

?Публичные поля классов


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

class Dog {
    constructor() {
        this.name = 'Tommy';
    }
}

Когда нужно было создать класс, который расширял бы некий родительский класс, необходимо было вызывать super() в конструкторе дочернего класса. Делать это нужно было до того, как к дочернему классу можно было бы добавлять его собственные свойства. Вот как это выглядит:

class Animal {}
class Dog extends Animal {
    constructor() {
        super(); // вызываем super перед использованием `this` в конструкторе
        this.sound = 'Woof! Woof!';
    }
    makeSound() {
        console.log( this.sound );
    }
}
// создаём экземпляр класса
const tommy = new Dog();
tommy.makeSound(); // Woof! Woof!

Благодаря появлению синтаксиса публичных полей класса можно описывать поля класса за пределами конструктора. Система при этом выполнит неявный вызов super().

class Animal {}
class Dog extends Animal {
    sound = 'Woof! Woof!'; // публичное поле класса
    makeSound() {
        console.log( this.sound );
    }
}
// создаём экземпляр класса
const tommy = new Dog();
tommy.makeSound(); // Woof! Woof!

При неявном вызове super() ему передаются все аргументы, предоставленные пользователем при создании экземпляра класса (это — стандартное поведение JavaScript, тут нет ничего особенного, связанного с приватными полями классов). Если конструктор родительского класса нуждается в аргументах, подготовленных особенным образом, нужно вызвать super() самостоятельно. Взглянем на результаты работы неявного вызова конструктора родительского класса при создании экземпляра дочернего класса.

class Animal {
    constructor( ...args ) {
        console.log( 'Animal args:', args );
    }
}
class Dog extends Animal {
    sound = 'Woof! Woof!'; // публичное поле класса
makeSound() {
        console.log( this.sound );
    }
}
// создаём экземпляр класса
const tommy = new Dog( 'Tommy', 'Loves', 'Toys!' );
tommy.makeSound(); // Animal args: [ 'Tommy', 'Loves', 'Toys!' ]

?Приватные поля классов


Как известно, в JavaScript нет модификаторов доступа к полям классов наподобие public, private или protected. Все свойства объектов по умолчанию являются публичными. Это означает, что доступ к ним ничем не ограничен. Ближе всего к тому, чтобы сделать некое свойство объекта подобным приватному свойству, можно подойти, используя тип данных Symbol. Это позволяет скрывать свойства объектов от внешнего мира. Возможно, вы пользовались именами свойств с префиксом _ (знак подчёркивания) для того чтобы указать на то, что соответствующие свойства нужно считать предназначенными лишь использования внутри объекта. Однако это — лишь нечто вроде уведомления для тех, кто будет пользоваться объектом. Это не решает проблему реального ограничения доступа к свойствам.

Благодаря механизму приватных полей классов можно сделать так, что свойства класса будут доступны лишь внутри этого класса. Это приводит к тому, что к ним нельзя обратиться извне и работая с экземпляром класса (объектом). Возьмём предыдущий пример и попробуем обратиться извне к свойству класса, при объявлении которого использовался префикс _.

class Dog {
    _sound = 'Woof! Woof!'; // это свойство должно считаться приватным
    
    makeSound() {
        console.log( this._sound );
    }
}
// создаём экземпляр класса
const tommy = new Dog();
console.log( tommy._sound ); // Woof! Woof!

Как видите, использование префикса _ не позволяет решить нашу проблему. Приватные поля классов можно объявлять так же, как и публичные, но вместо префикса в виде символа подчёркивания, к их именам надо добавлять префикс в виде символа решётки (#). Попытка несанкционированного доступа к объявленному подобным образом приватному свойству объекта приведёт к следующей ошибке:

SyntaxError: Undefined private field

Вот пример:

class Dog {
    #sound = 'Woof! Woof!'; // это - приватное свойство
    makeSound() {
        console.log( this.#sound );
    }
}
// создаём экземпляр класса
const tommy = new Dog();
tommy.makeSound() // Woof! Woof!
//console.log( tommy.#sound ); // SyntaxError

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

Объявлять приватные (и публичные) поля можно и не записывая в них некие значения:

class Dog {
    #name;
    constructor( name ) {
        this.#name = name;
    }
    showName() {
        console.log( this.#name );
    }
}
// создаём экземпляр класса
const tommy = new Dog( 'Tommy' );
tommy.showName(); // Tommy

> Поддержка


  • TC39: Stage 3
  • Chrome: 74+
  • Node: 12+

Метод строк .matchAll()


В прототипе типа данных String имеется метод .match(), который возвращает массив фрагментов строки, соответствующих условию, заданному регулярным выражением. Вот пример использования этого метода:

const colors = "#EEE, #CCC, #FAFAFA, #F00, #000";
const matchColorRegExp = /([A-Z0-9]+)/g;
console.log( colors.match( matchColorRegExp ) );
// Вывод:
["EEE", "CCC", "FAFAFA", "F00", "000"]

При использовании этого метода, однако, не даётся дополнительных сведений (вроде индексов) о найденных фрагментах строки. Если убрать флаг g из регулярного выражения, передаваемого методу .match() — он вернёт массив, в котором будут содержаться дополнительные сведения о результатах поиска. Правда, при таком подходе найден будет лишь первый фрагмент строки, соответствующий регулярному выражению.

const colors = "#EEE, #CCC, #FAFAFA, #F00, #000";
const matchColorRegExp = /#([A-Z0-9]+)/;
console.log( colors.match( matchColorRegExp ) );
// Вывод: (для удобства просмотра тут представлен сокращённый вариант вывода)
["#EEE", "EEE", index: 0, input: "<colors>"]

Для того чтобы получить нечто подобное, но уже для нескольких фрагментов строки, придётся пользоваться методом регулярных выражений .exec(). Конструкции, которые для этого нужны, сложнее, чем та, в которой для получения подобных результатов использовался бы вызов единственного метода строки. В частности, здесь нам понадобится цикл while, который будет выполняться до тех пор, пока .exec() не вернёт null. Пользуясь этим подходом, учитывайте то, что .exec() не возвращает итератор.

const colors = "#EEE, #CCC, #FAFAFA, #F00, #000";
const matchColorRegExp = /#([A-Z0-9]+)/g;
// в строгом режиме будет выдано сообщение об ошибке,
// Uncaught ReferenceError: match is not defined
while( match = matchColorRegExp.exec( colors ) ) {
  console.log( match );
}
// Вывод: (для удобства просмотра тут представлен сокращённый вариант вывода)
["#EEE", "EEE", index: 0, input: "<colors>"]
["#CCC", "CCC", index: 6, input: "<colors>"]
["#FAFAFA", "FAFAFA", index: 12, input: "<colors>"]
["#F00", "F00", index: 21, input: input: "<colors>"]
["#000", "000", index: 27, input: input: "<colors>"]

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

const colors = "#EEE, #CCC, #FAFAFA, #F00, #000";
const matchColorRegExp = /#([A-Z0-9]+)/g;
console.log( ...colors.matchAll( matchColorRegExp ) );
// Вывод: (для удобства просмотра тут представлен сокращённый вариант вывода)
["#EEE", "EEE", index: 0, input: "<colors>"]
["#CCC", "CCC", index: 6, input: "<colors>"]
["#FAFAFA", "FAFAFA", index: 12, input: "<colors>"]
["#F00", "F00", index: 21, input: input: "<colors>"]
["#000", "000", index: 27, input: input: "<colors>"]

> Поддержка


  • TC39: stage 4
  • Chrome: 73+
  • Node: 12+
  • Firefox: 67+

Именованные группы в регулярных выражениях


Концепция групп в JavaScript-реализации механизмов регулярных выражений немного отличается от реализации аналогичной концепции в других языках. А именно, когда, пользуясь JavaScript, RegEx-шаблон помещают в круглые скобки (за исключением тех случаев, когда круглые скобки используются для оформления ретроспективных или опережающих проверок), шаблон превращается в группу.

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

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

["#EEE", "EEE", index: 0, input: "<colors>"]

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

const str = "My name is John Doe.";
const matchRegExp = /My name is ([a-z]+) ([a-z]+)/i;
const result = str.match( matchRegExp );console.log( result );
// если константа result равна null - возникнет ошибка
console.log( { firstName: result[1], lastName: result[2] } );
// Вывод:
["My name is John Doe", "John", "Doe", index: 0, input: "My name is John Doe.", groups: undefined]
{firstName: "John", lastName: "Doe"}

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

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

const str = "My name is John Doe.";
const matchRegExp = /My name is (?<firstName>[a-z]+) (?<lastName>[a-z]+)/i;
const result = str.match( matchRegExp );
console.log( result );
console.log( result.groups );
// Вывод:
["My name is John Doe", "John", "Doe", index: 0, input: "My name is John Doe.", groups: {firstName: "John", lastName: "Doe"}]
{firstName: "John", lastName: "Doe"}

Надо отметить, что именованные группы нормально работают и вместе с методом .matchAll().

> Поддержка


  • TC39: Stage 4
  • Chrome: 64+
  • Node: 10+

Продолжение следует…

Уважаемые читатели! Пользовались ли вы уже какими-нибудь из описанных здесь новшеств JavaScript?

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


  1. sanchezzzhak
    20.08.2019 12:57

    Пользуюсь lockahead в node10, так как начиная с этой версии было доступны без флага гармонии.


    Приватным и свойствами объектов использую с 12 ноды, только в серверной коде


  1. Cryvage
    20.08.2019 14:03

    Когда уже сделают нормальную работу с юникодом в регулярках?


    1. EvilGenius18
      20.08.2019 17:30

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

      /\p{Lu}/gu

      или

      /\p{Uppercase}/gu


      1. Cryvage
        20.08.2019 23:16

        Ну например:

        "Я знаю, что Ваня хороший парень".search(/\bВаня\b/gu ); // выводит -1
        "I know that John is a nice guy".search(/\bJohn\b/gu); // выводит 12
        

        А для замены пресловутого "\w", приходится использовать конструкции вида:
        const notALetterRegEx = /[^A-Za-z?µ?A-OO-oo-??-??-????-??-??-???-???-??-??-??-??-???-??-??-??-??-??-???-??-??-????-??-???-??-???-????-??-??-??-??-??-??-???-????-??-??-??-??-??-??-??-??-??-??-???-??-??-??-??-??-??-????-??-??-??-??-??-??-???-??-????-??-??-??-???-??-??-??-???-??-??-??-??-???-??-??-??-??-??-??-????-??-??-??-??-???-??-??-??-??-???-??-??-??-??-???-????-??-??-????-??-??-???-???-???-??-??-??-???-??-???-??-??-???-??-???-??-??-??-??-??-???-??-??-??-??-??-???-??-??-??-??-??-??-??-??-??-??-??-??-??-??-??-??-??-????-??-???-??-??-??-??-??-??-??-??-??-??-??-??-??-??-??-??-??-??-?????-??-??-???-??-??-??-??-??-??-????-????-???-?????-??-?-??-???-??-??-??-??-??-??-??-???-??-??-??-??-??-??-??-??-???-??-??-??-??-??-??-??-??-??-??-??-??-??-??-??-??-??-??-??-??-??-??-??-??-??-??-??-??-??-??-??-??-??-??-??-??-??-??-??-???-??-??-???-??-??-??-??-??-??-??-??-?A-Za-z?-??-??-??-??-?]|[\ud840-\ud868][\udc00-\udfff]|\ud800[\udc00-\udc0b\udc0d-\udc26\udc28-\udc3a\udc3c-\udc3d\udc3f-\udc4d\udc50-\udc5d\udc80-\udcfa\ude80-\ude9c\udea0-\uded0\udf00-\udf1e\udf30-\udf40\udf42-\udf49\udf80-\udf9d\udfa0-\udfc3\udfc8-\udfcf]|\ud801[\udc00-\udc9d]|\ud802[\udc00-\udc05\udc08\udc0a-\udc35\udc37-\udc38\udc3c\udc3f\udd00-\udd15\udd20-\udd39\ude00\ude10-\ude13\ude15-\ude17\ude19-\ude33]|\ud808[\udc00-\udf6e]|\ud835[\udc00-\udc54\udc56-\udc9c\udc9e-\udc9f\udca2\udca5-\udca6\udca9-\udcac\udcae-\udcb9\udcbb\udcbd-\udcc3\udcc5-\udd05\udd07-\udd0a\udd0d-\udd14\udd16-\udd1c\udd1e-\udd39\udd3b-\udd3e\udd40-\udd44\udd46\udd4a-\udd50\udd52-\udea5\udea8-\udec0\udec2-\udeda\udedc-\udefa\udefc-\udf14\udf16-\udf34\udf36-\udf4e\udf50-\udf6e\udf70-\udf88\udf8a-\udfa8\udfaa-\udfc2\udfc4-\udfcb]|\ud869[\udc00-\uded6]|\ud87e[\udc00-\ude1d]/ug;
        

        Возможно у меня какое-то неправильное представление о полноценной поддержке Юникод, но я всегда думал что классы символов, вроде "\b", или "\w" должны отрабатывать одинаково для всех языков, или должны иметься Юникодовые аналоги.
        Может я просто чего-то не знаю? Буду только рад, если мне укажут на ошибку.


        1. EvilGenius18
          21.08.2019 13:20

          В вашем первом примере можно сделать вот так:
          regexr.com/4jj6t

          "Я знаю, что Ваня хороший парень".search(/Ваня/gu)

          А чтобы найти любую «букву» не обязательно перечислять все символы, достаточно указать просто параметр \p{Letter}, например:
          regexr.com/4jj7c
          /\p{Letter}+/gu


          Я не могу подсказать как работать с \b \w, сам не разбираюсь, но возможно примеры на вот этом сайте вам помогут понять как это делать: www.regular-expressions.info/unicode.html


          1. Cryvage
            22.08.2019 08:58

            В вашем первом примере можно сделать вот так:
            regexr.com/4jj6t
            "Я знаю, что Ваня хороший парень".search(/Ваня/gu)
            


            Ну вы же понимаете, что вопрос вовсе не в том, какими способами можно распарсить предложение из примера выше, а в том, что класс "\b", означающий границу слова, не работает, если в тексте присутствуют не латинские символы.
            А чтобы найти любую «букву» не обязательно перечислять все символы, достаточно указать просто параметр \p{Letter}

            К сожалению, пока только в Хроме и его производных. Так что пока рано отказываться от старых подходов. По крайней мере на фронтенде. На крайний случай есть библиотека XRegExp, если в проекте много работы с регулярными выражениями и не латинским текстом. Но ради пары регулярок, я её обычно не тащу.
            Но в целом согласен. По крайней мере, в этом направлении есть определённый прогресс. И это хорошо.
            Я не могу подсказать как работать с \b \w, сам не разбираюсь, но возможно примеры на вот этом сайте вам помогут понять как это делать: www.regular-expressions.info/unicode.html

            Вы упускаете суть проблемы. Разумеется, существуют альтернативные способы определения границы слова, различной степени сложности и изощрённости, которые можно применить для юникода. И для прочих классов тоже можно подобрать альтернативу, где-то краткую и изящную на основе всяких \p{L}, где-то сложную и громоздкую, но в то же время рабочую, что уже не плохо. Но проблема не в этом.
            Извиняюсь за тавтологию, но в данном случае, проблема в том, что вообще существует проблема, которой не должно быть. Ведь мы говорим о нормальной поддержке юникода, а не о хоть какой-нибудь. Классы символов, такие как \b, \w, \W и т.д., должны просто работать. В том числе с юникодом. Добавляя флаг u, я хочу от него именно такого поведения. Ведь это не только интуитивно и удобно, но и чисто практически, позволило бы использовать многие готовые регулярки, которые уже работают для латиницы. Кроме того, в других реализациях регулярных выражений, все эти классы: \b, \w, и т.д. — прекрасно работают с юникодом. Например, в C# они работают. В Qt они работают. Ну и конечно же, в Perl они работают:
            Character classes in regular expressions match based on the character properties specified in the Unicode properties database.
            \w can be used to match a Japanese ideograph, for instance;

            Так что, ждать такого поведения вполне естественно. Ведь это и есть нормальная работа с юникодом. Почему в JS это не реализовано, для меня загадка.


      1. Cryvage
        20.08.2019 23:17

        дубль


  1. AriesUa
    20.08.2019 15:45

    От синтаксиса приватных переменных все еще коробит. Почему решетка? Ведь есть нормальная запись «private». Интересно, чем было вызван такой синтаксис? Возможно под капотом используют Symbol, а решетка, как указатель. Но как по мне — это какой-то костыль.


    1. punkkk
      20.08.2019 17:38

      Читал где-то про такую позицию, что

      class Super {
          ...
          equals (toCompare) {
              return this.field === toCompare.field
          }
      }
      


      JS не имеет типизации и toCompare может быть чем угодно => либо такой вот костыль (с решеткой), либо каждый раз движку проверять, что toCompare является того же типа и тогда уже проверять приватное поле, иначе — проверять публичное поле, что как бы уже не очень, ибо два разных поведения у одного написания.

      Хотя меня все равно корежит от этой решеточки.


      1. kemsky
        20.08.2019 17:48

        Скорее это желание лучше минифицировать и обфусцировать)


    1. bodqhrohro
      20.08.2019 21:45
      +1

      А почему бы не использовать наконец для чего-то решётку? В древности с её помощью можно было при определении объекта назначить один и тот же дочерний объект нескольким его свойствам. Эту штуку объявили устаревшей, с тех пор праздно болталась… То же самое касается собаки: ни CoffeeScript'овский сахар для this, ни декораторы в стандарт так и не перекочевали.

      Впрочем, сама возможность умышленного сокрытия свойств, с целью не дать возможность пользователям библиотеки брать на себя ответственность за возможную поломку внутреннего состояния классов — спорная и пагубная практика. Ведь если припрёт, всё равно найдут способ это ограничение обойти — например, форкнуть библиотеку, и кому от этого хорошо? В других ООП-языках, когда идея инкапсуляции разбилась о суровую реальность, пришлось пост-фактум изобретать доступ к приватным свойствам через рефлексию. Странно, что JS в 2k19-м опять наступает на те же грабли.


      1. AriesUa
        20.08.2019 21:57
        +1

        По поводу собаки (@). Надеюсь что в будущем декораторы будут стандартом. В Typescript декораторы удобно использовать.


      1. Druu
        21.08.2019 10:08

        В других ООП-языках, когда идея инкапсуляции разбилась о суровую реальность

        Это когда и где она разбилась?


        пришлось пост-фактум изобретать доступ к приватным свойствам через рефлексию

        Рефлексия не для доступа к приватным полям нужна. К слову, она может этого доступа и не предоставлять в принципе.


        1. bodqhrohro
          22.08.2019 16:12

          Это когда и где она разбилась?
          Да ещё примерно после появления C++, когда попёрли реальные проекты на нём и стало ясно, что инкапсуляция может мешать. По этой причине в Python она не заложена вообще. В Java инкапсуляция есть, но почти с первой же версии (1.1) рефлексия появилась.
          Рефлексия не для доступа к приватным полям нужна
          Многие вещи используются не только для того, для чего изначально придуманы. Мало того: вещи, для которых предназначена рефлексия (сериализация и отладка), как раз и являются частными случаями, когда доступ к приватным полям таки нужен.
          К слову, она может этого доступа и не предоставлять в принципе.
          Может, но тогда она и непригодна в полной мере для своих задач. А где из языков с инкапсуляцией такое есть, к слову? В Java/C# могут быть ограничены права доступа, но с полными правами рефлексия полноценная. В PHP вроде вообще ограничений нет.


      1. movl
        22.08.2019 21:41

        Еще можно отпатчить код, скормить его eval и обойтись без форка. Но логика почему необходимо именно такая строгая инкапсуляция, по моему, очень простая.


        Если мы говорим про другие ООП языки, то видимо подразумеваем статически типизируемые, и где инкапсуляцию проверяет компилятор. В таком случае, нет ничего нелогичного в том, что можно отрефлексировать скрытые сущности во время выполнения, тут пользователь должен отдавать себе отчет куда он лезет. Однако для JS, где нет этапа компиляции и где мета-программирование самая обычная практика, возможность какого-либо взаимодействия со скрытыми сущностями во время выполнения, будет низводить все эти нововведения до конвенциональной инкапсуляции по типу: символ подчеркивания перед названием свойства или до объявления свойств через Symbol. Таким образом происходит потеря всякого смысла такой реализации.


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


        1. bodqhrohro
          23.08.2019 00:08

          Лицензия не всегда позволяет сделать форк. В мире JS редко, но встречаются проприетарные библиотеки. Да и с LGPL, по идее, могут возникнуть проблемы, если сломать в форке совместимость с апстримом.


          1. movl
            23.08.2019 10:01

            Я хотел показать, почему необходима именно такая реализация. И бессмысленность любой другой реализации как мне кажется вполне весомый аргумент. Тут уже скорее переходим к вопросу, нужна ли вообще инкапсуляция в JS. И с этим все очень просто: сейчас как правило для скрытия того, что действительно должно быть скрыто, используются Map или WeakMap, предложение к стандарту предлагает заменит это поведение на простую синтаксическую конструкцию, ну и плюс это поможет облегчить дебаг, а может и еще что-то. То есть все озвученные вами проблемы существуют и сейчас, и они никуда не денутся, однако при этом упростится участь разработчика, которому не придется городить лишнюю логику для реализации инкапсуляции. Короче этот стандарт нужно считать синтаксическим сахаром к реализации инкапсуляции через WeakMap.


            1. bodqhrohro
              24.08.2019 13:31

              Зачем такие извращения, если можно просто создать переменную в области видимости класса, к которой больше никто не имеет доступа? Например, завернув его в модуль, или, по старинке, в IIFE?


              1. movl
                24.08.2019 13:35

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


                1. bodqhrohro
                  24.08.2019 13:44

                  Классические конструкторы объектов (функции, которые напихивают свойства/методы объекта в свой this) лишены и этого недостатка. Можно объявлять переменные внутри конструктора: они будут доступны в методах и при этом уникальны для каждого объекта. Так что проблема, выходит, в ES6-ном сахаре для классов.


                  1. movl
                    24.08.2019 13:53

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


                    1. bodqhrohro
                      24.08.2019 14:41

                      Ну цимес-то в том, что классика даёт куда больше возможностей. Полноценная функция — на то и полноценная функция. Например, можно в зависимости от входных данных присваивать в качестве метода две разные функции, чтобы не заводить в объекте лишнее свойство, в зависимости от которого выбирается вариант метода при каждом его вызове. Можно даже формировать одним конструктором объекты с разной структурой. Всю эту гибкость class-сахар не даст никогда. Затея, КМК, сугубо для программистов, переучивающихся с других языков с привычной непрототипной реализацией ООП. Прототипное ООП мощнее, но и освоить его сложнее, и выстрелить себе в ногу проще.


                      1. movl
                        24.08.2019 15:35

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


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


                        Касательно снижения планки сложности перехода с другого языка, возможно этот вопрос и поднимался (не следил за обсуждениями). Но как я уже и говорил выше, это решение скорее является сахаром к той практике, которая сложилась с WeakMap уже сейчас. Ничего принципиально нового этот синтаксис не вносит. И под капотом, судя по всему, это будет реализовываться именно так, как это уже делают.


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


      1. alexahdp
        22.08.2019 23:11

        Полностью согласен. Есть интересный случай из практики: на соседнем проекте был криво реализован oauth. Мы взяли какую-то стандартную нодовскую библиотеку для этих целей и пытались использовать, но потом выяснилось, что под наш случай она не подходит. И, хвала небесам, все, что было необходимо сделать, чтобы библиотека заработала — вызвать недокументированную приватную функцию с кастомными параметрами. Это не потребовало от нас форкать библиотеку, городить костыли для доступа к данным,… Просто сделали вызов функции и оставили комментарий с тремя восклицательными знаками почему здесь вызывается эта функция. И это позволило нам дальше участвовать в хакатоне, не тратя время понапрасну.

        Ну и еще к слову о приватности: никогда не понимал как тестировать приватные методы. Приватный метод вполне может содержать нетривиальную логику и его надо обязательно протестировать. А городить костыли в виде пубичных методов, которые существуют единственно для того, чтобы вызвать приватный — уже какое-то убожество.

        В принципе, возможность создать метод с подчеркивание и обозначить таким образом, что он приватный или дописав перед ним jsdoc-нотацию private мне всегда нравилась. При необходимости такие методы можно вызвать из консоли, их легко протестировать, легко отлаживать код, вызывая при необходимости такие методы прямо в дебаггере. Ну а то, что кто-то упрямо использует то, что ему сказали не использовать — ну сорян, сам мудак)


        1. bodqhrohro
          23.08.2019 00:14

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


    1. Paul_Smith
      21.08.2019 10:47

      Я присутствовал на докладе комиссии TC39 в Берлине этим летом. Задал именно этот вопрос. Ответ был — потому что TypeScript набрал очень большую популярность и уже использует keyword «private». Как по мне, это очень странное решение, но Microsoft играет довольно большую роль в развитии JS, видимо им приходится договариваться.



  1. Mox
    21.08.2019 01:43

    Блин, а почему не сделали подчеркивание то — ведь де факто его и использовали для обозначения private данных


    1. kshshe
      21.08.2019 08:38

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


  1. Zoolander
    21.08.2019 08:42

    Обозначение private через решетку?
    Означает ли это, что хеш-объект для хранения цветов в формате CSS станет нерабочим?

    Другими словами, вызов colorHash["#dedede"] станет нерабочим?

    Если да, то ждите поломок — хеши цветов используются, хотя и не так часто.


    1. movl
      21.08.2019 18:28

      Нет. Подобные обращения останутся рабочими. Синтаксис предполагает обращение только через точку (this.#private), что в текущем стандарте является синтаксической ошибкой.


      1. Zoolander
        22.08.2019 08:47

        спасибо