Pull to refresh

Новые возможности ECMAScript 2021-2023

Level of difficultyHard
Reading time13 min
Views12K

Привет, я Мельникова Ирина - frontend разработчик в компании Астрал-Софт.

Сегодня я хотела бы поговорить об обновлениях ECMAScript, традиционно публикуемых в июне. Буквально недавно версия стандарта 2023 года перешла в статус freezed, что означает, что никакие изменения в нее вноситься уже не будут.

Поэтому сейчас самое время обсудить что нового произошло в стандарте за последние годы и что нас ожидает уже в этом году.

ES5, ES2016, ECMAScript 2019 — как разобраться во всем этом?

ECMAScript new proposals 2021, 2022, 2023, новые возможности ES
ECMAScript new proposals 2021, 2022, 2023, новые возможности ES

ECMAScript — это стандарт, на котором основан JavaScript, его часто называют ES.

ECMAScript (/ˈɛkməskrɪpt/) (или ES) является языком программирования общего назначения , стандартизирован ассоциацией Ecma International согласно документу ECMA-262 . Это стандарт JavaScript, предназначенный для обеспечения взаимодействия веб-страниц в разных веб-браузерах.

ES vs JS, ECMAScript vs JavaScript
ES vs JS, ECMAScript vs JavaScript

Следует четко понимать, что ES не равен JS.

ECMAScript — Описанная в ECMA-262 спецификация создания скриптового языка общего назначения.

«ECMA-262» — это название и стандарта, и спецификации скриптового языка ECMAScript.

ECMAScript содержит правила, сведения и рекомендации, которые должны соблюдаться скриптовым языком, чтобы он считался совместимым с ECMAScript.

JavaScript — cкриптовый язык общего назначения, соответствующий спецификации ECMAScript.

Это “диалект” языка ECMAScript.

Таким образом, из спецификации ECMAScript вы узнаете, как создать скриптовый язык, а из документации JavaScript — как использовать скриптовый язык.

Как сказано в ECMA-262, JavaScript в основном реализует спецификацию ECMAScript, но с некоторыми отличиями.

Курица или яйцо?

JavaScript был создан в 1996 году. В 1997 году Ecma International предложила стандартизировать JavaScript, и в результате появился ECMAScript. Но поскольку JavaScript соответствует спецификации ECMAScript, JavaScript является примером реализации ECMAScript.

Получается, что ECMAScript основан на JavaScript, а JavaScript основан на ECMAScript.

ES vs JS, ECMAScript vs JavaScript
ES vs JS, ECMAScript vs JavaScript

Надеюсь данный факт вас немного позабавил, а для лучшего понимания исторической последовательности развития стандартов JavaScript до 2021 года можете почитать мою статью ECMAScript 2015, 2016, 2017, 2018, 2019, 2020, 2021

ECMAScript2021 (ES12)

String.prototype.replaceAll()

Заменяет все вхождения строки другим строковым значением.

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

"1 2 1 2 1 2".replace(/2/g, "0");
// → '1 0 1 0 1 0'

Благодаря replaceAll эту конструкцию можно заменить на:

// String.prototype.replaceAll(searchValue, replaceValue)

"1 2 1 2 1 2".replaceAll("2", "0");
// → '1 0 1 0 1 0'

Если аргумент поиска — пустая строка, вернется замещенное значение между каждой единицей кода UCS-2 / UTF-16.

"x".replace("", "_");
// → '_x'

"xxx".replace(/(?:)/g, "_");
// → '_x_x_x_'

"xxx".replaceAll("", "_");
// → '_x_x_x_

Promise.any() и AggregateError

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

Метод Promise.any() принимает итерируемый объект содержащий объекты Promise. Как только один из промисов выполнится успешно (fulfill), метод вернет единственный объект Promise со значением выполненного промиса.

На примере 3 промиса, которые завершаются в случайное время. Promise.any() вернет результат первого успешно завершившегося промиса среди p1p2 и p3.

const p1 = new Promise((resolve, reject) => {    
  setTimeout(() => resolve("A"), Math.floor(Math.random() * 1000));
});

const p2 = new Promise((resolve, reject) => {    
  setTimeout(() => resolve("B"), Math.floor(Math.random() * 1000));
});

const p3 = new Promise((resolve, reject) => {    
  setTimeout(() => resolve("C"), Math.floor(Math.random() * 1000));
});

const result = await Promise.any([p1, p2, p3]);
console.log(result); // Печатает "A", "B" или "C"

Если все промисы завершатся с ошибкой (rejected), тогда возвращённый Promise будет отклонён (rejected) с одним из значений: массив содержащий причины ошибки (отклонения), или AggregateError — подкласс Error, который объединяет выброшенные ошибки вместе.

const p1 = new Promise((resolve, reject) => reject('Причина ошибки 1'));
const p2 = new Promise((resolve, reject) => reject('Причина ошибки 2'));

Promise.any([p1, p2])    
  .catch((error) => {console.log(error, error.errors)});

В демонстрационных целях в Promise.any() передается только один промис. И этот промис завершается «с ошибкой» (reject). Приведенный выше код выведет следующую ошибку в консоли:

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

Объединяют оператор логического присваивания с логическими операциями &&, || или ??.

const a = { duration: 50, title: '', count: 1, repeated: 0 };

// Logical OR assignment (||=)
a.duration ||= 10;
console.log(a.duration); // 50

a.title ||= 'title is empty.';
console.log(a.title); // "title is empty"

// Logical AND assignment (&&=)
a.count &&= 2;
console.log(a.count); // 2

a.repeated &&= 2;
console.log(a.repeated); // 0

// Nullish coalescing assignment (??=)
a.duration ??= 10;
console.log(a.duration); // 50

a.speed ??= 25;
console.log(a.speed); // 25

Numeric separators

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

Такой синтаксис работает и с дробными частями, и с экспонентами и с BigInt:

1000000000   // Это миллиард? Или сто миллионов? Или это десять миллионов?
101475938.38 // Какая тут степень 10?

const FEE = 12300;
// Это 12,300 рублей? Или 123, потому что рассчитывается в копейках?

const AMOUNT = 1234500;
// Это 1,234,500? Или если в копейках, то  12,345?
// Или это финансовые расчеты с 4 знаками после запятой, тогда это 123.45?

1_000_000_000           // Теперь ясно, что это миллиард
101_475_938.38          // А это сотня миллиардов

let fee = 123_00;       // ₽123 (12300 копеек, очевидно)
let fee = 12_300;       // ₽12,300 (воу, вот это платеж!)
let amount = 12345_00;  // 12,345 (1234500 копейки)
let amount = 123_4500;  // 123.45 (4 знака после запятой)
let amount = 1_234_500; // 1,234,500

0.000_001 // одна миллионная
1e10_000  // 10^10000

WeakRef

Слабые ссылки (Weak References).

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

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

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

Например, если у вас есть несколько больших бинарных объектов изображений (например, представленных как ArrayBuffers), вы можете захотеть связать имя с каждым изображением.

Существующие структуры данных просто не позволяют решить данную задачу:

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

  • WeakMaps для этой цели тоже не подходят: они “слабы” по своим ключам, но в данном случае нам нужна структура, “слабая” по своим значениям.

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

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;  
  };
}

var getImageCached = makeWeakCached(getImage);

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

Один из способов решить эту проблему — периодически очищать кеш и удалять мертвые записи. Другой способ — финализаторы.

Финализаторы

FinalizationRegistry — это дополнительная функция WeakRef. Она позволяет регистрировать коллбеки, которые будут вызываться после того, как объект был забран сборщиком мусора.

const registry = new FinalizationRegistry((value) => {    
  console.log(value);
});

(function () {    
  const obj = {};    
  registry.register(obj, "Backbencher");
})();

Здесь registry является экземпляром FinalizationRegistry. Коллбэк, переданный в FinalizationRegistry, срабатывает при сборке мусора.

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

Финализаторы — сложная штука, и лучше их избегать. Они могут вызываться в неожиданное время или вообще не вызываться:

  • Финализаторы не вызываются при закрытии вкладки браузера или при выходе из процесса;

  • не помогают сборщику мусора выполнять свою работу; скорее, они являются помехой;

  • нарушают внутренний учет сборщика мусора;

  • Finalizable объекты почти всегда представляют объем выделения памяти, который невидим для сборщика мусора. Таким образом фактическое использование ресурсов системой с финализируемыми объектами выше, чем это должно быть по мнению сборщика мусора.

ECMAScript2022 (ES13)

Приватные методы

Приватные методы могут быть доступны только внутри класса, в котором они определены. Имена приватных методов начинаются с символа #.

class Person {        
  // Приватный метод    
  #setType() {        
    console.log("I am Private");    
  }    
  
  // Публичный метод    
  show() {        
    this.#setType();    
  }
}

const personObj = new Person();
personObj.show(); // "I am Private"
personObj.#setType(); // TypeError: personObj.setType is not a function

Поскольку setType() является приватным методом, personObj.setType возвращает значение undefined. Попытка использовать undefined в качестве функции вызывает ошибку TypeError.

Приватные методы (как и приватные свойства) не наследуются.

Приватные аксессоры

Функции-аксессоры (get/set) можно сделать приватными, добавив # к имени функции.

class Person {    
  // Публичные аксессоры    
  get name() { return "Backbencher" }    
  set name(value) {}    
  
  // Приватные аксессоры    
  get #age() { return 42 }    
  set #age(value) {}
}

const obj = new Person();
console.log(obj.name); // "Backbencher"
console.log(obj.age); // undefined

В приведенном выше коде ключевые слова get и set делают name аксессором. Несмотря на то, что name выглядит как функция, его можно читать как обычное свойство.

Статические блоки инициализации внутри класса

Обеспечивают более гибкую логику инициализации, чем статические свойства, такие как использование try…catch или установка нескольких полей из одного значения. Инициализация выполняется в контексте объявления текущего класса с доступом к приватному состоянию, что позволяет классу обмениваться информацией о своих приватных свойствах с другими классами или функциями, объявленными в той же области видимости.

// without static blocks:
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 = ...;
}

// with static blocks:
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 = ...;    
    }  
  }
}

Метод at() для Array, String и TypedArray

В течение многих лет программисты просили предоставить возможность выполнять «отрицательную индексацию» массивов, как это можно сделать в других языках программирования, например, в Python, где отрицательные числа отсчитываются в обратном порядке от последнего элемента.

К сожалению, дизайн языка JS делает невозможным использовать синтаксис [] с отрицательными числами, т.к. он не специфичен для массивов и строк, а относится ко всем объектам.

Ссылка на значение по индексу, например arr[1], на самом деле просто указывает на свойство объекта с ключом «1», который может иметь любой объект.

Таким образом, arr[-1] уже “работает” в сегодняшнем коде, но он возвращает значение свойства “-1” объекта, а не индекс, отсчитывающий от конца.

Поэтому было выдвинуто предложение добавить метод .at() к Array, String и TypedArray, который принимает целочисленное значение и возвращает элемент по этому индексу с семантикой отрицательного числа, как описано выше.

Такое нововведение не только решает давний запрос, но также помогает избавиться от некоторых проблем для различных API DOM, описанные подробнее в репозитории tc39.

const arr = [1,2,3,4]
arr.at(-2) // 3

const str = "1234"
str.at(-2) // '3'

Error cause + Chaining Errors

Свойство .cause в объекте ошибки позволяет указать дополнительную информацию о возникшей ошибке.

try {  
  //Выполняем какое-то действие, которое выбросит ошибку  
  doSomeComputationThatThrowAnError() 
} catch (error) {  
  throw new Error('Я результат другой ошибки', { cause: error })
}

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

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

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

Следует отметить, что данное решение пока не идеально: Свойство .cause никак не задекларировано стандартом, туда можно вносить абсолютно любую информацию, так как эта информация не типизируема.

Верхнеуровневый await

Когда имеет смысл иметь модуль, ожидающий загрузки асинхронной операции? Вот несколько примеров использования:

  • Возможность загружать модули динамическиПозволяет модулям использовать значения времени выполнения для определения зависимостей. Полезно для таких вещей, как разделение dev/prod сред, разделение environment-ов, интернационализация и т.д.

const serviceName = await fetch("https://example.ru/some-service")
const service = await import(`/services/${serviceName}.js`)

// ИЛИ
const params = new URLSearchParams(location.search);
const theme = params.get('theme');
const stylingFunctions = await import(`/styling-functions-${theme}.js`);
  • Возможность загружать модули условно.

const date = new Date()

if(date.getFullYear() === 2023) {  
  await require('/special-code-for-2023-year.js')
}
  • Инициализация ресурсаПозволяет модулям представлять ресурсы, а также выдавать ошибки в тех случаях, когда модуль никогда не сможет быть использован.

const connection = await dbConnector();
  • Резервные копии зависимостей.

let importantLibrary;
try {  
  importantLibrary = await import('https://expl-a.com/importantLibrary');
} 
catch {  
  importantLibrary = await import('https://expl-b.com/importantLibrary');
}

Индексы совпадения регулярных выражений

Предоставляют дополнительную информацию о начальных и конечных индексах захваченных подстрок относительно входной строки.

const re1 = /a+(?<Z>z)?/d;

// indices are relative to start of the input string:
const s1 = "xaaaz";
const m1 = re1.exec(s1);
m1.indices[0][0] === 1;
m1.indices[0][1] === 5;
s1.slice(...m1.indices[0]) === "aaaz";

Доступный Object.prototype.hasOwnProperty()

Добавлен метод Object.hasOwn с тем же поведением, что и при вызове hasOwnProperty.call

let object = { foo: false }
Object.hasOwn(object, "foo") // true

let object2 = Object.create({ foo: true })
Object.hasOwn(object2, "foo") // false

let object3 = Object.create(null)
Object.hasOwn(object3, "foo") // false

Это полезное улучшение т.к. Object.prototype иногда может быть недоступен или переопределен, например ESLint имеет встроенное правило для запрета использования встроенных прототипов, таких как hasOwnProperty.

ECMAScript2023 (ES14)

Традиционно обновления ECMAScript публикуются в июне. Но уже сейчас можно посмотреть 14-ю редакцию ECMAScript от 2023, которая сейчас находится в статусе freezed, т.е. никакие изменения в нее вноситься уже не будут.

В данной версии спецификации не такой большой список изменений, что абсолютно нормально когда мы переходим на годовой цикл релиза.

Итак, спецификация 2023 года представила нам методы toSorted, toReversed, with, findLast и findLastIndex для Array.prototypeи TypedArray.prototype,
а также метод toSpliced для Array.prototype.

Кроме того добавлена поддержка #! комментарии в начале файлов для облегчения запуска исполняемых файлов ECMAScript.

и разрешено использовать Symbols в качестве ключей в WeakMap.

Подробнее обо всем можно почитать в официальном репозитории организации Ecma International TC39.

.findlast() и .findLastIndex() для Array и TypedArray

Предназначены для поиска элементов в массиве.

Работают так же, как .find() и .findIndex(), но итерируются в обратном порядке.

const array = [{ value: 1 }, { value: 2 }, { value: 3 }, { value: 4 }];

array.find(n => n.value % 2 === 1); // { value: 1 }
array.findIndex(n => n.value % 2 === 1); // 0

// ======== Before the proposal =========== 
// find
[...array].reverse().find(n => n.value % 2 === 1); // { value: 3 }

// findIndex
array.length - 1 - [...array].reverse().findIndex(n => n.value % 2 === 1); // 2
array.length - 1 - [...array].reverse().findIndex(n => n.value === 42); // should be -1, but 4

// ======== In the proposal =========== 
// find
array.findLast(n => n.value % 2 === 1); // { value: 3 }
// findIndex
array.findLastIndex(n => n.value % 2 === 1); // 2
array.findLastIndex(n => n.value === 42); // -1

Изменение массива, путем возвращения новой копии

Обновление для Array.prototype и TypedArray.prototype, позволяющее включать изменения в массив, путем возвращения новой копии.

Это предложение вводит следующие свойства функции

в Array.prototype:

  • Array.prototype.toReversed() -> Array

  • Array.prototype.toSorted(compareFn) -> Array

  • Array.prototype.toSpliced(start, deleteCount, ...items) -> Array

  • Array.prototype.with(index, value) -> Array

в TypedArray.prototype:

  • TypedArray.prototype.toReversed() -> TypedArray

  • TypedArray.prototype.toSorted(compareFn) -> TypedArray

  • TypedArray.prototype.with(index, value) -> TypedArray

Все эти методы сохраняют целевой массив нетронутым и вместо этого возвращают его копию с выполненным изменением.

Symbols как ключи для WeakMap

В качестве ключей для WeakMap можно будет использовать символы, сейчас для этих целей можно использовать только объекты.

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

const weak = new WeakMap();

// Pun not intended: being a symbol makes it become a more symbolic key
const key = Symbol('my ref');
const someObject = { /* data data data */ };

weak.set(key, someObject);

Hashbang Grammar #!

Синтаксис, присущий многим языкам программирования.

Унифицированный механизм удаления шебангов для некоторых хостов CLI JS перед передачей исходных кодов в движки JavaScript.

Дает возможность запускать исходники в Linux, Unix системах путем вставки определёной строки с указанием нужного интерпретатора.

#!/usr/bin/env node
// in the Script Goal
'use strict';
console.log(1);

#!/usr/bin/env node
// in the Module Goal
export {};
console.log(1);

ECMAScript 2024

На официальном сайте уже опубликован драфт на 2024 год, но какие изменения в него войдут пока достоверно неизвестно.

ES, ECMAScript 2024
ES, ECMAScript 2024

Мы можем посмотреть какие фичи находятся в stage 3 прямо сейчас в официальном репозитории:

ES ECMAScript Stage 3 Proposals
ES ECMAScript Stage 3 Proposals

Список нововведений подготовлен на основе ECMAScript Finished Proposals

Поддержку функций, перечисленные в статье можно проверить на сайте ECMAScript compatibility table.

При написании статьи использованы следующие ресурсы:

  1. Официальный сайт ecma-international

  2. Справочник по JavaScript

  3. ECMAScript 2015, 2016, 2017, 2018, 2019, 2020, 2021

  4. JavaScript ES2021 / ES12 новые возможности

  5. 4 важных нововведения в ES2022

Tags:
Hubs:
Total votes 16: ↑15 and ↓1+14
Comments4

Articles