Привет, друзья! Не за горами 2022 год, а это значит, что пришло время познакомиться с новыми возможностями, которыми нас порадует ECMAScript2022.


Вот о чем мы поговорим в этой статье:


  • await верхнего уровня
  • метод at() для индексируемых сущностей
  • метод hasOwn() для объектов
  • флаг d для регулярных выражений
  • 5 предложений для классов (специальные проверки для частных полей, блоки статической инициализации и др.)

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


await верхнего уровня


Скоро у нас появится возможность использовать ключевое слово await на верхнем уровне (top level). Под верхним уровнем в данном случае подразумевается область видимости (scope) модуля.


Модуль — это JS-файл, который импортируется в другой JS-файл либо подключается к странице с помощью тега script с атрибутом type="module" и содержит в себе код определенной части программы. Для модулей даже предусмотрено специальное расширение .mjs (использовать его необязательно).


В Node.js верхнеуровневый await можно использовать, начиная с версии 14.8.0 (август 2020 г.). На самом деле, данную возможность можно было использовать и до этого, но тогда требовалось передавать специальный флаг --harmony-top-level-await в командной строке при запуске приложения и, разумеется, использовать его можно было только в среде для разработки.


Соответствующий Node.js-файл должен иметь расширение .mjs либо в ближайшем package.json должно содержаться поле type со значением module:


import connectToMongoDb from './mongo/connect.js'
import { MONGO_URI } from './config/index.js'

await connectToMongoDb(MONGO_URI)

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


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


Именованная функция:


// импортируем функцию для обработки из другого модуля
import { process } from './some-module.js'
// создаем переменную для результата
let output

// создаем именованную функцию
async function main() {
 // динамически импортируем модуль
 const dynamic = await import(computedModuleSpecifier)
 // получаем данные от сервера
 const data = await fetch(url)
 // вычисляем результат
 // модуль экспортируется по умолчанию, т.е. с помощью `export default`
 output = process(dynamic.default, data)
}
// вызываем функцию
main()
// экспортируем результат
export { output }

IIFE:


import { process } from './some-module.js'
let output
// `IIFE`
;(async () => {
 const dynamic = await import(computedModuleSpecifier)
 const data = await fetch(url)
 output = process(dynamic.default, data)
})()
export { output }

Верхнеуровневый await позволяет обойтись без создания дополнительной (лишней) функции:


import { process } from './some-module.js'
const dynamic = await import(computedModuleSpecifier)
const data = await fetch(url)
export const output = process(dynamic.default, data)

Здорово, правда?


Вот статья, в которой подробно рассказывается про использование await верхнего уровня в JavaScript.


Метод at()


Метод at() предназначен для получения элементов индексируемых сущностей по отрицательным индексам по аналогии с тем, как это реализовано, например, в Python. К индексируемым сущностям относятся массивы, типизированные массивы и строки.


Сейчас для доступа к таким элементам мы вычитаем позицию элемента из длины массива (свойство length; в действительности, дело не в позиции элемента, а в том, что последний индекс массива на 1 меньше его длины по причине того, что индексация начинается с 0, а длина с 1):


const arr = [1, 2, 3, 4, 5]
// получаем первый элемент массива, начиная с конца
const firstLastEl = arr[arr.length - 1]
console.log(firstLastEl) // 5
// получаем второй элемент с конца
const secondLastEl = arr[arr.length - 2]
console.log(secondLastEl) // 4
// и т.д.

Вот как это будет выглядеть с at():


const arr = [1, 2, 3, 4, 5]
const firstLastEl = arr.at(-1)
const secondLastEl = arr.at(-2)

Мелочь, а приятно.


В описании предложения приводится соответствующий полифил. Рассмотрим его на примере массива:


// функция принимает число
function at(n) {
 // округляем число до целого, просто отбрасывая десятичную часть
 // значением по умолчанию является `0`
 n = Math.trunc(n) || 0
 // если получившееся число меньше `0`,
 // прибавляем к нему длину массива
 // если число равняется `-1`, а массив имеет длину `5`,
 // получаем `5 + -1` или `5 - 1`, или `4` - последний индекс
 // `this` в данном случае указывает (ссылается) на массив
 if (n < 0) n += this.length
 // если число меньше `0` или больше длины массива,
 // возвращаем `undefined` - индикатор отсутствия элемента с указанным индексом в массиве
 if (n < 0 || n > this.length) return undefined
 // возвращаем элемент
 return this[n]
}

// добавляем новый метод в прототип массива, т.е. для всех (будущих) массивов
Object.defineProperty(Array.prototype, 'at', {
 value: at,
 // метод доступен для записи
 writable: true,
 // не является перечисляемым
 enumerable: false,
 // является настраиваемым
 configurable: true
})

Хотите кусочек метапрограммирования? Пожалуйста.


Вот как можно реализовать доступ к элементу по отрицательному индексу с помощью объекта Proxy:


const arr = [1, 2, 3, 4, 5]

// возьмем логику полифила
const _arr = new Proxy(arr, {
 // target - цель проксирования
 get(target, index) {
   index = Math.trunc(index) || 0
   if (index < 0) index += target.length
   if (index < 0 || index > target.length) return undefined
   return target[index]
 }
})

console.log(_arr[-1]) // 5
console.log(_arr[-3]) // 3
console.log(_arr[-6]) // undefined

Метод hasOwn()


Метод hasOwn() предназначен для того, чтобы сделать метод hasOwnProperty() "более доступным". Что это означает?


Метод Object.prototype.hasOwnProperty() используется для проверки, содержит ли объект определенное свойство:


const obj = {
 prop: 'val'
}
console.log(
 obj.hasOwnProperty('prop')
) // true

Но что если у объекта нет метода hasOwnProperty()?


const obj = Object.create(null)

console.log(
 obj.hasOwnProperty('prop')
) // Uncaught TypeError: obj.hasOwnProperty is not a function

Получаем ошибку.


А что если кто-то взял и перезаписал метод hasOwnProperty?


const obj = {
 prop: 'val',
 hasOwnProperty: () => null
}

console.log(
 obj.hasOwnProperty('prop')
) // null

Не совсем то, что мы ожидали получить, верно?


Во многих библиотеках для решения названных проблем используется такая конструкция:


const hasProp = Object.prototype.hasOwnProperty

const obj = {
 prop: 'val'
}

// метод `call()` используется для выполнения функции или метода в нужном контексте -
// `this` внутри функции будет ссылаться на объект, переданный `call()` в качестве первого аргумента
// второй и последующий аргументы, передаваемые `call()`,
// это параметры функции
if (hasProp.call(obj, 'prop')) {
 console.log('obj has prop')
} // obj has prop

// объект без прототипа
const obj2 = Object.create(null)
console.log(
 hasProp.call(obj2, 'prop')
) // false

// объект с кастомным методом `hasOwnProperty()`
const obj3 = {
 prop: 'val',
 hasOwnProperty: () => null
}
console.log(
 hasProp.call(obj3, 'prop')
) // true

Вообще перезаписывать встроенные свойства и методы считается очень плохой практикой — никогда так не делайте!


С помощью метода hasOwn() безопасно определять наличие у объекта определенного свойства можно будет так:


const obj = {
 prop: 'val'
}
if (Object.hasOwn(obj, 'prop')) {
 console.log('obj has prop')
} // obj has prop

const obj2 = Object.create(null)
console.log(
 Object.hasOwn(obj2, 'prop')
) // false

const obj3 = {
 prop: 'val',
 hasOwnProperty: () => null
}
console.log(
 Object.hasOwn(obj3, 'prop')
) // true

Индексы совпадений


Флаг d в регулярном выражении предназначен для получения индексов совпадений (match indices).


Индексы совпадений — это начальный и конечный индексы захваченной подстроки (captured substring) по отношению к началу строки для поиска.


Проще показать.


В следующем примере мы используем метод matchAll() для нахождения всех вхождений подстроки с некоторой дополнительной информацией:


const str = 'one1'
// без флага `d`
// ищем число
const match = str.matchAll(/one(\d)/g)
console.log(...match)
/*
[
 0: 'one1'
 1: '1'
 groups: undefined
 index: 0
 input: 'one1'
]
*/

// с флагом `d`
const matchIndices = str.matchAll(/one(\d)/dg)
console.log(...matchIndices)
/*
 // то же самое +
 indices: Array(2)
   // начальный и конечный индексы строки
   0: [0, 4]
   // начальный и конечный индексы захваченной подстроки
   1: [3, 4]
*/

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


Классы


Дальнейшему развитию классов посвящено целых 5 предложений.


Сегодня мы можем определять в классе следующее:


  • публичные (открытые) поля экземпляров в конструкторе
  • частные (закрытые) поля экземпляров в конструкторе
  • публичные методы экземпляров
  • публичные статические методы (методы классов)

Схематично это можно представить следующим образом:


class C {
 constructor() {
   this.publicInstanceField = 'Публичное поле экземпляра'
   this.#privateInstanceField = 'Частное поле экземпляра'
 }

 publicInstanceMethod() {
   console.log('Публичный метод экземпляра')
 }

 // публичный метод для получения значения частного поля экземпляра
 getPrivateInstanceField() {
   console.log(this.#privateInstanceField)
 }

 static publicClassMethod() {
   console.log('Публичный статический метод (метод класса)')
 }
}

const c = new C()

console.log(c.publicInstanceField) // Публичное поле экземпляра

// при попытке прямого доступа к частному полю выбрасывается исключение
// console.log(c.#privateInstanceField) // SyntaxError: Private field '#privateInstanceField' must be declared in an enclosing class

c.getPrivateInstanceField() // Частное поле экземпляра

c.publicInstanceMethod() // Публичный метод экземляра

C.publicClassMethod() // Публичный статический метод (метод класса)

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


Теперь перейдем непосредственно к предложениям.


Определение полей классов


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


В следующем примере из описания предложения создается пользовательский элемент num-counter со значением счетчика в качестве текстового содержимого. Клик по счетчику приводит к увеличению его значения на 1 (обратите внимание, что значение счетчика является закрытым полем):


class Counter extends HTMLElement {
 #x = 0

 clicked() {
   this.#x++
   window.requestAnimationFrame(this.render.bind(this))
 }

 constructor() {
   super()
   this.onclick = this.clicked.bind(this)
 }

 connectedCallback() { this.render() }

 render() {
   this.textContent = this.#x.toString()
 }
}
window.customElements.define('num-counter', Counter)

Частные методы и геттеры/сеттеры


Второе предложение позволяет определять частные методы и геттеры/сеттеры экземпляров.


Следующий пример из описания предложения похож на предыдущий, за исключением того, что в нем используются частные геттер и сеттер для счетчика и метод для увеличения его значения (clicked()) стал закрытым:


class Counter extends HTMLElement {
 #xValue = 0

 get #x() { return #xValue }
 set #x(value) {
   this.#xValue = value
   window.requestAnimationFrame(this.#render.bind(this))
 }

 #clicked() {
   this.#x++
 }

 constructor() {
   super()
   this.onclick = this.#clicked.bind(this)
 }

 connectedCallback() { this.#render() }

 #render() {
   this.textContent = this.#x.toString()
 }
}
window.customElements.define('num-counter', Counter)

Статические возможности классов


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


В следующем примере из описания предложения в классе сначала определяется 3 статических частных поля, соответствующих 3 основным цветам — красному, зеленому и синему. Затем определяется статический метод для доступа к цвету по названию:


class ColorFinder {
 static #red = '#ff0000'
 static #green = '#00ff00'
 static #blue = '#0000ff'

 static colorName(name) {
   switch (name) {
     case 'red': return ColorFinder.#red
     case 'blue': return ColorFinder.#blue
     case 'green': return ColorFinder.#green
     default: throw new RangeError('Неизвестный цвет!')
   }
 }

 // Как-то используем `colorName`
}

Таким образом, мы получим почти полный комплект инструментов для работы с классами. Почему почти? Ну, для полного комплекта не хватает, как минимум, защищенных (protected) полей и методов, которые, в отличие от частных, будут наследоваться экземплярами. Вероятно, именно в этом направлении будет идти дальнейшее развитие ООП в JavaScript.


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


Да, имеется еще 2 предложения, посвященных классам, но они не кажутся мне слишком интересными, поэтому я оставил их на закуску.


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


Предположим, что нам необходимо выполнить какие-то вычисления при инициализации класса (например, с помощью try/catch) или установить два поля на основе одного значения.


Сейчас это приходится делать за пределами класса:


class C {
 static x = ...
 static y
 static z
}

try {
 const obj = doSomethingWith(C.x)
 C.y = obj.y
 C.z = obj.z
} catch {
 C.y = ...
 C.z = ...
}

Блоки статической инициализации позволяют реализовать такую логику внутри инициализируемого класса:


class C {
 static x = ...
 static y
 static z

 static {
   try {
     const obj = doSomethingWith(this.x)
     this.y = obj.y
     this.z = obj.z
   } catch {
     this.y = ...
     this.z = ...
   }
 }
}

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


Данное предложение в определенном смысле расширяет идею предыдущего.


Частные поля имеют встроенную специальную проверку (brand check), которая выбрасывает исключение при попытке получить доступ к несуществующему частному полю объекта.


Как можно безопасно выполнить такую проверку?


С помощью блока статической инициализации и try/catch это можно сделать следующим образом:


class C {
 #brand

 static isC(obj) {
   try {
     obj.#brand;
     return true
   } catch {
     return false
   }
 }
}

console.log(C.isC({})) // false
console.log(C.isC(new C())) // true

Но что если у нас имеется такой геттер:


class C {
 #data = null

 get #getter() {
   // при отсутствии данных в момент вызова геттера выбрасывается исключение
   if (!this.#data) {
     throw new Error('Данные отсутствуют!')
   }
   return this.#data
 }

 static isC(obj) {
   try {
     obj.#getter
     return true
   } catch {
     return false
     // несмотря на наличие закрытого геттера, мы попадаем в блок `catch`
     // из-за того, что он выбрасывает исключение
   }
 }
}

Рассматриваемое предложение позволяет безопасно проверять наличие частных полей и методов с помощью ключевого слова in:


class C {
 #brand

 #method() {}

 get #getter() {}

 static isC(obj) {
   return #brand in obj && #method in obj && #getter in obj
 }
}

Пожалуй, это все, чем я хотел поделиться с вами в этой статье.


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


Если вы хотите узнать про возможности, появившиеся в JavaScript в этом году, рекомендую взглянуть на эту статью.


Благодарю за внимание и хорошего дня!




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


  1. kai3341
    14.09.2021 18:36

    Множественное наследование с миксинами так и не завезли. Или я не умею гуглить?


  1. Alexandroppolus
    14.09.2021 20:33
    +5

    Object.hasOwn и позиции групп в регулярках - годная вещь (из разряда: редко бывает нужно, но если вдруг понадобилось, то гемор и костыли). По классам так себе фичи, всё равно используется TS.