Привет, друзья! Не за горами 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 в этом году, рекомендую взглянуть на эту статью.
Благодарю за внимание и хорошего дня!
ссылка на оригинал статьи https://habr.com/ru/articles/577760/

Добавить комментарий