Привет, друзья! Не за горами 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)
Alexandroppolus
14.09.2021 20:33+5Object.hasOwn и позиции групп в регулярках - годная вещь (из разряда: редко бывает нужно, но если вдруг понадобилось, то гемор и костыли). По классам так себе фичи, всё равно используется TS.
kai3341
Множественное наследование с миксинами так и не завезли. Или я не умею гуглить?