11 удивительных функций Javascript в ES13
Published: 2022-08-29 17:11:42

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

Давайте рассмотрим последние нововведения, добавленные в ECMAScript 2022 (ES13), и посмотрим примеры их реализации, чтобы лучше понять их.

**1. Class Field Declaration**

До ES13 поля класса могли быть инициализированы только в специальном методе `constructor()`. В отличие от других языков, мы не можем объявить или определить их во внешней области класса.
```
class Person {
  constructor() {
    this.name = 'Alex';
    this.age = 22;
  }
}
const person = new Person();
console.log(person.name); // Alex
console.log(person.age); // 22
```
ES13 убирает это ограничение. Теперь мы можем инициализировать поля без метода, следующим образом:

```
class Person {
  name = 'Alex';
  age = 22;
}
const person = new Person();
console.log(person.name); // Alex
console.log(person.age); // 22
```
**2. Приватные методы и поля**
Во многих других языках, также существуют «защищённые» поля, доступные только внутри класса или для дочерних классов.

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

```
class Person {
  _firstName = 'Ivan';
  _lastName = 'Sidorov';
  get name() {
    return `${this._firstName} ${this._lastName}`;
  }
}
const person = new Person();
console.log(person.name); //  Ivan Sidorov
// Участники, которые должны быть приватными, все еще доступны
// вне класса

console.log(person._firstName); // Ivan
console.log(person._lastName); // Sidorov

// Здесь мы можем изменить участника
person._firstName = 'Oleg';
person._lastName = 'Ivanov';
console.log(person.name); // Oleg Ivanov
```
С ES13 мы теперь можем добавлять приватные поля в класс, добавив к нем хэштег (#). Теперь любая попытка получить к ним доступ извне класса приведет к ошибке: 

```
class Person {
  #firstName = 'Ivan';
  #lastName = 'Sidorov';
  get name() {
    return `${this.#firstName} ${this.#lastName}`;
  }
}
const person = new Person();
console.log(person.name);
// SyntaxError: Private field '#firstName' must be
// declared in an enclosing class
console.log(person.#firstName);
console.log(person.#lastName);
```
Обратите внимание, что ошибка, вызванная здесь, является синтаксической ошибкой, которая возникает во время компиляции, поэтому код ниже не выполняется. Компилятор не ожидает, что вы попытаетесь получить доступ к приватным полям извне класса, поэтому он предполагает, что вы пытаетесь объявить один из них.

**3. Await на верхнем уровне**
В JavaScript оператор `await` используется для приостановки выполнения асинхронной функции и ожидает до тех пор, пока обещание не будет выполнено или отклонено.

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

```
function setTimeoutAsync(timeout) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, timeout);
  });
}
// SyntaxError: await is only valid in async functions
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()**
Обычно мы используем квадратные скобки `([])` в JavaScript для доступа к `N`-му элементу массива.

```
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`-му элементу из конца массива, мы просто передаем отрицательное значение `-N` в `at()`.

```
const arr = ['a', 'b', 'c', 'd'];
// 1ый элемент с конца
console.log(arr.at(-1)); // d
// 2ой элемент с конца
console.log(arr.at(-2)); // c
```
Помимо массива, этим свойством обладают String и TypedArray.

```
const str = 'Hello World';
console.log(str.at(-1)); // d
console.log(str.at(-2)); // l
const typedArray = new Uint8Array([16, 32, 48, 64]);
console.log(typedArray.at(-1)); // 64
console.log(typedArray.at(-2)); // 48
```

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

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

```
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);
```
Теперь мы можем указать флаг `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 is not a function
console.log(obj.hasOwnProperty('color'));
```

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

```
const obj = Object.create(null);
obj.color = 'green';
obj.age = 2;
obj.hasOwnProperty = () => false;
console.log(Object.prototype.hasOwnProperty.call(obj, 'color')); // true
console.log(Object.prototype.hasOwnProperty.call(obj, 'name')); // 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. Error Cause**
Объекты ошибок теперь имеют `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.findLast((item) => item.value === 'y');
const foundIndex = letters.findLastIndex((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
```

**Вывод**
Итак, мы увидели новые функции, которые ES13 привносит в JavaScript. Используйте их, чтобы повысить свою продуктивность как разработчика и писать красивый и чистый код.