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

от автора

Привет, друзья! Не за горами 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/