JavaScript: о том, что нас ждет в следующем году

Kate

Administrator
Команда форума
vspsylgwmocx0i2e_strgkxm_ik.png

Привет, друзья! Не за горами 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 в этом году, рекомендую взглянуть на эту статью.

 
Сверху