Как стать автором
Обновить

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

Время на прочтение7 мин
Количество просмотров8.4K


Доброго времени суток, друзья!

Данная статья посвящена возможностям JavaScript, которые будут представлены в новой версии спецификации (ECMAScript 2021, ES12).

Речь пойдет о следующем:

  • String.prototype.replaceAll()
  • Promise.any()
  • WeakRefs
  • Операторы логического присваивания
  • Разделители чисел


String.prototype.replaceAll()


String.prototype.replaceAll() (предложение Mathias Bynens ) позволяет заменять все экземпляры подстроки в строке другим значением без использования глобального регулярного выражения.

В следующем примере мы заменяем все символы "+" запятыми с пробелом с помощью регулярного выражения:

const strWithPlus = 'один+два+три'
const strWithComma = strWithPlus.replace(/\+/g, ', ')
// один, два, три

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

Существует и другой подход, основанный на использовании методов String.prototype.split() и Array.prototype.join():

const strWithPlus = 'один+два+три'
const strWithComma = strWithPlus.split('+').join(', ')
// один, два, три

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

String.prototype.replaceAll() решает названные проблемы и предоставляет простой и удобный способ глобальной замены подстрок:

const strWithPlus = 'один+два+три'
const strWithComma = strWithPlus.replaceAll('+', ', ')
// один, два, три

Обратите внимание, что в целях обеспечения согласованности с предшествующими API поведение String.prototype.replaceAll(searchValue, newValue) (searchValue — искомое значение, newValue — новое значение) аналогично поведению String.prototype.replace(searchValue, newValue), за исключением следующего:

  • Если искомым значением является строка, тогда replaceAll заменяет все совпадения, а replace — только первое
  • Если искомое значение является неглобальным регулярным выражением, то replace заменяет первое совпадение, а replaceAll выбрасывает исключение во избежание противоречия между отсутствием флага «g» и названием метода (replace all — заменить все [совпадения])

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

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

const whiteSpaceHell = ' люблю   нажимать  пробел    '

И мы хотим заменить два и более пробелов на один. Можно ли решить данную проблему с помощью replaceAll? Нет.

Посредством String.prototype.trim() и replace с глобальным регулярным выражением эта задача решается так:

const whiteSpaceNormal =
  whiteSpaceHell
    .trim()
    .replace(/\s{2,}/g, ' ')
    // \s{2,} означает два и более пробела
    // люблю нажимать пробел

Promise.any()


Promise.any() (предложение Mathias Bynens, Kevin Gibbons и Sergey Rubanov ) возвращает значение первого выполненного промиса. При отклонении всех промисов, переданных Promise.any() в качестве аргумента (в виде массива), выбрасывается исключение «AggregateError».

AggregateError — это новый подкласс Error, группирующий отдельные ошибки. Каждый экземпляр AggregateError содержит ссылку на массив с исключениями.

Рассмотрим пример:

const promise1 = new Promise((resolve, reject) => {
  const timer = setTimeout(() => {
    resolve('p1')
    clearTimeout(timer)
  }, ~~(Math.random() * 100))
}) // ~~ - это альтернатива Math.floor()

const promise2 = new Promise((resolve, reject) => {
  const timer = setTimeout(() => {
    resolve('p2')
    clearTimeout(timer)
  }, ~~(Math.random() * 100))
})

;(async() => {
  const result = await Promise.any([promise1, promise2])
  console.log(result) // p1 или p2
})()

Результатом будет значение первого разрешенного промиса.

Пример из предложения:

Promise.any([
  fetch('https://v8.dev/').then(() => 'home'),
  fetch('https://v8.dev/blog').then(() => 'blog'),
  fetch('https://v8.dev/docs').then(() => 'docs')
]).then((first) => {
  // Любой (первый) из выполненных промисов
  console.log(first);
  // → 'home'
}).catch((error) => {
  // Если все промисы отклонены
  console.log(error);
})


Обратите внимание, что Promise.race(), в отличие от Promise.any(), возвращает значение первого разрешенного промиса независимо от того, выполнен он или отклонен.

WeakRefs


WeakRefs (weak references — слабые ссылки) (предложение Dean Tribble, Mark Miller, Till Schneidereit и др. ) предоставляет две новые возможности:

  • Создание слабых ссылок на объект с помощью класса «WeakRef»
  • Запуск пользовательских финализаторов (finalizers) после уничтожения объектов сборщиком мусора с помощью класса FinalizationRegistry

Если очень коротко, то WeakRef позволяет создавать слабые ссылки на объекты, являющиеся значениями свойств другого объекта, а финализаторы могут использоваться, в том числе, для удаления ссылок на «очищенные» сборщиком мусора объекты.

Данная техника может быть полезна при создании функции запоминания (мемоизации), которая используют встроенный кэш для предотвращения повторного выполнения функции при наличии в кэше вычисленного значения для переданного функции аргумента (при условии использования объектов в качестве значений свойств объекта-кэша и риске их последующего удаления).

Как вы помните, причина появления в JavaScript такой структуры, как Map (хэш-таблица), помимо более высокой скорости поиска значения по ключу, заключалась в том, что ключами обычного объекта могут быть только строки или символы. Map же в качестве ключа позволяет использовать любые типы данных, включая объекты.

Однако вскоре обнаружилась проблема, связанная с утечками памяти: удаление объектов, являющихся ключами Map, не делало эти объекты недостижимыми (алгоритм «mark-and-sweep» — пометка и удаление), что не позволяло сборщику мусора уничтожать их, освобождая занимаемую ими память.

Другими словами, объекты, используемые в качестве ключей в Map, сохраняются навсегда.

Для решения этой проблемы была представлена другая структура — WeakMap (и WeakSet). Отличием WeakMap от Map является то, что ссылки на объекты-ключи в WeakMap являются слабыми: удаление таких объектов позволяет сборщику мусора перераспределить выделенную для них память.

Таким образом, рассматриваемое предложение представляет собой очередной этап в развитии хэш-таблицы в JavaScript. Теперь объекты могут использоваться как в качестве ключей, так и в качестве значений в других объектах без риска возникновения утечек памяти.

Еще раз, если речь идет о создании встроенного кэша:

  • При отсутствувии риска возникновения утечек памяти следует использовать Map
  • При использовании объектов-ключей, которые могут быть впоследствии удалены, следует использовать WeakMap
  • При использовании объектов-значений, которые могут быть впоследствии удалены, следует использовать Map совместно с WeakRef

Пример последнего случая из предложения:

function makeWeakCached(f) {
  const cache = new Map()
  return key => {
    const ref = cache.get(key)
    if (ref) {
      // обратите внимание на эту строку
      const cached = ref.deref()
      if (cached !== undefined) return cached;
    }

    const fresh = f(key)
    // и на эту (объяснение ниже)
    cache.set(key, new WeakRef(fresh))
    return fresh
  };
}

const getImageCached = makeWeakCached(getImage);

  • Конструктор WeakRef принимает аргумент, который должен быть объектом, и возвращает слабую ссылку на него
  • Метод deref экземпляра WeakRef возвращает одно из двух значений:

В случае со встроенным кэшем, финализатор предназначен для завершения процесса очистки после уничтожения объекта-значения сборщиком мусора, или, проще говоря, для удаления слабой ссылки на такой объект.

function makeWeakCached(f) {
  const cache = new Map()
  // функция очистки кэша - удаления слабых ссылок
  const cleanup = new FinalizationRegistry(key => {
    const ref = cache.get(key)
    if (ref && !ref.deref()) cache.delete(key)
  })

  return key => {
    const ref = cache.get(key)
    if (ref) {
      const cached = ref.deref()
      if (cached !== undefined) return cached
    }

    const fresh = f(key)
    cache.set(key, new WeakRef(fresh))
    // запись нового объекта в реестр (его регистрация)
    cleanup.register(fresh, key)
    return fresh
  }
}

const getImageCached = makeWeakCached(getImage);

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

Операторы логического присваивания


Операторы логического присваивания (предложение Justin Ridgewell и Hemanth HM) представляют собой комбинацию из логических операторов (&&, ||, ??) и выражений присваивания.

На сегодняшний день в JavaScript имеются следующие операторы присваивания:

=
оператор присваивания

+=
присваивание со сложением

-=
присваивание с вычитанием

/=
присваивание с делением

*=
присваивание с умножением

&&=
присваивание с логическим И

||=
присваивание с логическим ИЛИ

??=
присваивание с проверкой на нулевое значение (null и undefined - нулевые значение, 0, false, и '' - ненулевые значения)

**=
присваивание с возведением в степень

%=
присваивание с делением с остатком

&=
присваивание с побитовым И

|=
присваивание с побитовым ИЛИ

^=
присваивание с побитовым исключающим ИЛИ

<<=
присваивание с побитовым сдвигом влево

>>=
присваивание с побитовым сдвигом вправо

>>>=
присваивание с побитовым сдвигом вправо с заполнением нулями

операторы деструктурирующего присваивания
[a, b] = [ 10, 20 ]
{a, b} = { a: 10, b: 20 }

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

a ||= b
// аналогично: a || (a = b)
// присвоение осуществляется только в случае, когда значение "a" является ложным

a &&= b
// аналогично: a && (a = b)
// присвоение осуществляется только в случае, когда значение "a" является истинным

a ??= b
// аналогично: a ?? (a = b)
// присвоение осуществляется только в случае, когда значение "a" является нулевым (null или undefined)

Пример из предложение:

// без операторов логического присваивания
function example(opts) {
  // выглядит хорошо, но может вызвать сеттер
  opts.foo = opts.foo ?? 'bar'

  // не вызывает сеттер, но выглядит не очень хорошо
  opts.baz ?? (opts.baz = 'qux')
}

example({ foo: 'foo' })

// с операторами логического присваивания
function example(opts) {
  // сеттеры не вызываются без необходимости
  opts.foo ??= 'bar'

  // отсутствует "подготовка" в виде opts.baz
  opts.baz ??= 'qux';
}

example({ foo: 'foo' })

Разделители чисел


Разделители чисел (предложение Christophe Porteneuve) или, если точнее, разделители цифр в числах позволяют добавлять символ подчеркивания (_) между цифрами для повышения читаемости чисел.

Например:

const num = 100000000
// какое значение имеет переменная num? 1 млрд? 100 млн? 10 млн?

Разделители решают эту проблему:

const num = 100_000_000 // правильный ответ: 100 млн

Разделители могут использоваться как в целой, так и в десятичной частях числа:

const num = 1_000_000.123_456

Разделители могут использоваться не только в целых числах и числах с плавающей точкой, но также в бинарных, шестнадцатиричных, восьмиричных и BigInt литералах.

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

Хотите проверить или освежить свои знания по JavaScript? Тогда обратите внимание на мое замечательное приложение (сам себя не похвалишь...).

Надеюсь, вы нашли для себя что-нибудь интересное. Благодарю за внимание.
Теги:
Хабы:
Всего голосов 14: ↑12 и ↓2+10
Комментарии1

Публикации

Истории

Работа

Ближайшие события