11 новых возможностей JavaScript в ES13

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

Вспомним самые последние фичи, добавленные в ECMAScript 2022 (ES13). Чтобы оценить эффективность этих нововведений, рассмотрим примеры их использования.

1. Объявление поля класса

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

class Car {
  constructor() {
    this.color = 'blue';
    this.age = 2;
  }
}

const car = new Car();
console.log(car.color); // синий
console.log(car.age); // 2

ES13 снял это ограничение. Теперь можно писать код следующим образом:

class Car {
  color = 'blue';
  age = 2;
}

const car = new Car();
console.log(car.color); // синий
console.log(car.age); // 2

2. Приватные методы и поля

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

class Person {
  _firstName = 'Joseph';
  _lastName = 'Stevens';

  get name() {
    return `${this._firstName} ${this._lastName}`;
  }
}

const person = new Person();
console.log(person.name); // Джозеф Стивенс

// Члены, предназначенные для закрытого доступа, все еще могут быть доступны
// за пределами класса
console.log(person._firstName); // Джозеф
console.log(person._lastName); // Стивенс

// Их также можно изменить
person._firstName = 'Robert';
person._lastName = 'Becker';

console.log(person.name); // Роберт Беккер

В ES13 можно добавлять приватные поля и члены в класс с помощью хэштега (#). Попытка получить к ним доступ извне класса приведет к ошибке:

class Person {
  #firstName = 'Joseph';
  #lastName = 'Stevens';

  get name() {
    return `${this.#firstName} ${this.#lastName}`;
  }
}

const person = new Person();
console.log(person.name);

// SyntaxError: Приватное поле '#firstName' должно быть
// объявлено во включающем классе
console.log(person.#firstName);
console.log(person.#lastName);

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

3. Оператор await на верхнем уровне

В JavaScript оператор await используется для приостановки выполнения до тех пор, пока Promise не будет решен (выполнен или отклонен).

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

function setTimeoutAsync(timeout) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, timeout);
  });
}

// SyntaxError: await доступен только в функциях async
await setTimeoutAsync(3000);

В ES13 можно сделать так:

function setTimeoutAsync(timeout) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, timeout);
  });
}

// Ожидается тайм-аут - ошибка не возникает 
await setTimeoutAsync(3000);

4. Статические поля класса и статические приватные методы

В ES13 можно объявлять статические поля и статические приватные методы для класса. Статические методы могут обращаться к другим приватным/публичным статическим членам класса с помощью ключевого слова this, а методы экземпляра могут обращаться к ним с помощью this.constructor.

class Person {
  static #count = 0;

  static getCount() {
    return this.#count;
  }

  constructor() {
    this.constructor.#incrementCount();
  }

  static #incrementCount() {
    this.#count++;
  }
}

const person1 = new Person();
const person2 = new Person();

console.log(Person.getCount()); // 2

5. Статический блок класса

ES13 позволяет определять блоки static, которые будут выполняться только один раз  —  при создании класса. Это похоже на статические конструкторы в других языках с поддержкой объектно-ориентированного программирования, таких как C# и Java.

Класс может иметь любое количество блоков инициализации static {} в своем теле. Они будут выполняться вместе с любыми чередующимися статическими инициализаторами полей в том порядке, в котором объявлены. Для доступа к свойствам суперкласса можно использовать свойство super в блоке static.

class Vehicle {
  static defaultColor = 'blue';
}

class Car extends Vehicle {
  static colors = [];

  static {
    this.colors.push(super.defaultColor, 'red');
  }

  static {
    this.colors.push('green');
  }
}

console.log(Car.colors); // [ 'blue', 'red', 'green' ]

6. Эргономичный способ обнаружения приватных полей

Эту новую функцию можно использовать для проверки наличия в объекте конкретного приватного поля с помощью оператора in.

class Car {
  #color;

  hasColor() {
    return #color in this;
  }
}

const car = new Car();
console.log(car.hasColor()); // true;

Оператор in может корректно различать приватные поля с одинаковыми именами из разных классов:

class Car {
  #color;

  hasColor() {
    return #color in this;
  }
}

class House {
  #color;

  hasColor() {
    return #color in this;
  }
}

const car = new Car();
const house = new House();

console.log(car.hasColor()); // true;
console.log(car.hasColor.call(house)); // false
console.log(house.hasColor()); // true
console.log(house.hasColor.call(car)); // false

7. Метод at() для индексирования

Традиционно для доступа к N-му элементу массива в JavaScript используются квадратные скобки ([]), что не является особо обременительным. Мы просто обращаемся к свойству N - 1 массива.

const arr = ['a', 'b', 'c', 'd'];
console.log(arr[1]); // b

Однако, чтобы получить доступ к N-му элементу с конца массива с квадратными скобками, потребуется использовать индекс arr.length - N:

const arr = ['a', 'b', 'c', 'd'];

// 1-й элемент с конца
console.log(arr[arr.length - 1]); // d

// 2-й элемент с конца
console.log(arr[arr.length - 2]); // c

Новый метод at() позволяет сделать это более лаконично и выразительно. Чтобы получить доступ к N-му элементу с конца массива, нужно просто передать в at()отрицательное значение -N.

const arr = ['a', 'b', 'c', 'd'];

// 1-й элемент с конца
console.log(arr.at(-1)); // d

// 2-й элемент с конца
console.log(arr.at(-2)); // c

Помимо массивов, строки и объекты TypedArrayтакже теперь обладают методом at().

const str = 'Coding Beauty';
console.log(str.at(-1)); // y
console.log(str.at(-2)); // t

const typedArray = new Uint8Array([16, 32, 48, 64]);
console.log(typedArray.at(-1)); // 64
console.log(typedArray.at(-2)); // 48

8. Индексы совпадений RegExp

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

Ранее можно было получить только начальный индекс совпадений regex в строке.

const str = 'sun and moon';

const regex = /and/;

const matchObj = regex.exec(str);

// [ 'and', index: 4, input: 'sun and moon', groups: undefined ]
console.log(matchObj);

Теперь можно указать regex-флаг d, чтобы получить два индекса совпадений  —  начальный и конечный.

const str = 'sun and moon';

const regex = /and/d;

const matchObj = regex.exec(str);

/**
[
  'and',
  index: 4,
  input: 'sun and moon',
  groups: undefined,
  indices: [ [ 4, 7 ], groups: undefined ]
]
 */
console.log(matchObj);

При установленном флаге d возвращаемый объект будет иметь свойство indices, содержащее начальный и конечный индексы.

9. Метод Object.hasOwn()

В JavaScript можно использовать метод Object.prototype.hasOwnProperty() для проверки наличия у объекта заданного свойства.

class Car {
  color = 'green';
  age = 2;
}

const car = new Car();

console.log(car.hasOwnProperty('age')); // true
console.log(car.hasOwnProperty('name')); // false

Но с этим подходом связаны определенные проблемы. Например, метод Object.prototype.hasOwnProperty() не защищен: его можно переопределить, определив пользовательский метод hasOwnProperty() для класса с совершенно иным поведением, чем Object.prototype.hasOwnProperty().

class Car {
  color = 'green';
  age = 2;

  // Данный метод не указывает на то, имеет ли объект 
  // этого класса заданное свойство 
  hasOwnProperty() {
    return false;
  }
}

const car = new Car();

console.log(car.hasOwnProperty('age')); // false
console.log(car.hasOwnProperty('name')); // false

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

const obj = Object.create(null);
obj.color = 'green';
obj.age = 2;

// TypeError: obj.hasOwnProperty - это не функция
console.log(obj.hasOwnProperty('color'));

Один из способов решения этих проблем  —  использовать вызов метода call() в отношении свойства Object.prototype.hasOwnProperty Function, например:

const obj = Object.create(null);
obj.color = 'green';
obj.age = 2;
obj.hasOwnProperty = () => false;

Это не очень удобно. Чтобы не повторяться, можно написать многоразовую функцию:

function objHasOwnProp(obj, propertyKey) {
  return Object.prototype.hasOwnProperty.call(obj, propertyKey);
}

const obj = Object.create(null);
obj.color = 'green';
obj.age = 2;
obj.hasOwnProperty = () => false;

console.log(objHasOwnProp(obj, 'color')); // true
console.log(objHasOwnProp(obj, 'name')); // false

Однако в этом нет необходимости, поскольку теперь можно использовать новый встроенный метод Object.hasOwn(). Как и приведенная выше многоразовая функция, он принимает в качестве аргументов объект и свойство и возвращает true, если указанное свойство является прямым свойством объекта. В противном случае возвращается false.

const obj = Object.create(null);
obj.color = 'green';
obj.age = 2;
obj.hasOwnProperty = () => false;

console.log(Object.hasOwn(obj, 'color')); // true
console.log(Object.hasOwn(obj, 'name')); // false

10. Причина ошибки

Объекты ошибок теперь имеют свойство cause для указания исходной ошибки, которая привела к возникновению данной. Это помогает добавить дополнительную контекстную информацию к ошибке и помочь в диагностике неожиданного поведения. Можно указать причину ошибки, установив свойство cause на объект, переданный в качестве второго аргумента конструктору Error().

function userAction() {
  try {
    apiCallThatCanThrow();
  } catch (err) {
    throw new Error('New error message', { cause: err });
  }
}

try {
  userAction();
} catch (err) {
  console.log(err);
  console.log(`Cause by: ${err.cause}`);
}

11. Поиск в массиве по последнему элементу

В JavaScript уже есть метод Array find() для поиска в массиве элемента, который удовлетворяет заданному условию проверки. Аналогично, используется метод findIndex() для поиска индекса такого элемента. В то время как find() и findIndex() начинают поиск с первого элемента массива, бывают случаи, когда предпочтительнее начать поиск с последнего.

Есть сценарии, в которых поиск с последнего элемента может обеспечить лучшую производительность. Попытаемся, например, получить элемент массива со свойством value, равным y, используя find() и findIndex():

const letters = [
  { value: 'v' },
  { value: 'w' },
  { value: 'x' },
  { value: 'y' },
  { value: 'z' },
];

const found = letters.find((item) => item.value === 'y');
const foundIndex = letters.findIndex((item) => item.value === 'y');

console.log(found); // { value: 'y' }
console.log(foundIndex); // 3

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

const letters = [
  { value: 'v' },
  { value: 'w' },
  { value: 'x' },
  { value: 'y' },
  { value: 'z' },
];

const found = letters.findLast((item) => item.value === 'y');
const foundIndex = letters.findLastIndex((item) => item.value === 'y');

console.log(found); // { value: 'y' }
console.log(foundIndex); // 3

В другом случае может потребоваться специальный поиск в массиве с конца, чтобы получить нужный элемент. Например, если мы хотим найти последнее четное число в списке чисел, find() и findIndex() дадут неверный результат:

const nums = [7, 14, 3, 8, 10, 9];

// дает 14 вместо 10
const lastEven = nums.find((value) => value % 2 === 0);

// дает 1 вместо 4
const lastEvenIndex = nums.findIndex((value) => value % 2 === 0);

console.log(lastEven); // 14
console.log(lastEvenIndex); // 1

Чтобы изменить порядок элементов перед вызовом find() и findIndex(), можно вызвать метод reverse() для массива. Но такой подход вызвал бы ненужную мутацию массива, поскольку метод reverse() меняет местами элементы массива. Единственным способом избежать такой мутации было бы создание новой копии всего массива, что может вызвать проблемы с производительностью, если речь идет о больших массивах.

Кроме того, findIndex() все равно не будет работать с перевернутым массивом, так как перевертывание элементов означает изменение индексов, которые они имели в исходном массиве. Чтобы получить исходный индекс, пришлось бы выполнить дополнительное вычисление, что означает написание большего количества кода.

const nums = [7, 14, 3, 8, 10, 9];

// Копирование всего массива с синтаксисом spread
// до вызова reverse()
const reversed = [...nums].reverse();

// правильно дает 10
const lastEven = reversed.find((value) => value % 2 === 0);

// дает 1 вместо 4
const reversedIndex = reversed.findIndex((value) => value % 2 === 0);

// Необходимо пересчитать, чтобы получить исходный индекс
const lastEvenIndex = reversed.length - 1 - reversedIndex;

console.log(lastEven); // 10
console.log(reversedIndex); // 1
console.log(lastEvenIndex); // 4

Именно в таких случаях пригодятся методы findLast() и findLastIndex().

const nums = [7, 14, 3, 8, 10, 9];

const lastEven = nums.findLast((num) => num % 2 === 0);
const lastEvenIndex = nums.findLastIndex((num) => num % 2 === 0);

console.log(lastEven); // 10
console.log(lastEvenIndex); // 4

Этот код короче и более читабелен. А главное, он дает правильный результат.

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Coding Beauty: 11 Amazing New JavaScript Features in ES13

Предыдущая статьяКак удалять локальные ветки с помощью псевдонимов Git
Следующая статья5 рекомендаций по оптимизации запросов SQL