Новшества JavaScript: итоги Google I-O 2019. Часть 1

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

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

Новшества JavaScript: итоги Google I-O 2019. Часть 1 - 1

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

Регулярные выражения (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?

Новшества JavaScript: итоги Google I-O 2019. Часть 1 - 2

Автор: ru_vds

Источник


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