ES13(ES2022)中11个新特性解读

前端开发·教程·资源 · 2022-08-24

ES13(ES2022)中11个新特性解读

与其他大多数编程语言一样,JavaScript 也在不断发展中,每年都会增加很多新特性和功能来变得更强大,让开发人员能够编写更富有表现力和更简洁的代码。
ECMAScript 2022 Features.png

让我们了解一下ECMAScript 2022 (ES13)中添加的最新特性,并查看它们的使用示例,以便更好地理解它们。

1. 类字段声明

在ES13之前,类字段只能在构造函数中声明。与大部分语言不同,我们不能在类的最外层作用域声明或定义它们。

类字段:字段是对象的属性,状态的表示,又称为域 域变量 成员变量 字段变量
class Car {
    constructor() {
        this.color = 'blue';
        this.age = 2;
    }
}

const car = new Car();
console.log(car.color); // blue
console.log(car.age); // 2

ES13 移除了这个限制。现在我们可以这样编写代码:

class Car {
  color = 'blue';
  age = 2;
}
const car = new Car();
console.log(car.color); // blue
console.log(car.age); // 2

2. 私有方法和字段变量

以前,无法在类中声明私有成员。传统上,私有成员的前缀是一个下划线( _ ),以表明它是私有的,但事实上仍然可以从类外部访问和修改它。

class Person {
    _firstName = 'We';
    _lastName = 'Gon'; get name() {
        return `${this._firstName} ${this._lastName}`;
    }
}
const person = new Person();

// 仍可以访问私有成员
console.log(person.name); // We Gon

// 在Class类外部访问
console.log(person._firstName); // We
console.log(person._lastName); // Gon

// 仍可以被更改
person._firstName = 'Ghosts';
person._lastName = 'Becker'; 

console.log(person.name); // Ghosts Becker

在ES13中,我们现在可以向类添加私有字段和成员,方法是在类前面加上标签( # ),试图从类外部访问它们将导致错误:

class Person {
    #firstName = 'We';
    #lastName = 'Gon'; get name() {
        return `${this.#firstName} ${this.#lastName}`;
    }
}
const person = new Person();
// Uncaught SyntaxError:Private field '#firstName' must be declared in an enclosing class
console.log(person.name); 

// 属性 "#lastName" 在类 "Person" 外部不可访问,因为它具有专用标识符。
console.log(person.#firstName); // We
console.log(person.#lastName); // Gon
在VsCode编辑器会提示:属性 "#lastName" 在类 "Person" 外部不可访问,因为它具有专用标识符。

请注意,这里抛出的错误是一个语法错误,发生在编译时,因此不能运行代码的任何部分。

3. await运算符

在JavaScript中,await操作符用于暂停执行,直到Promise被解决 (完成 / 拒绝)。

以前,只能在async函数中使用await操作符——该函数使用async 关键字声明。无法在全局范围内直接使用await

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);
    });
}
// Waits for timeout - no error thrown
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允许定义静态块,该块只会在创建时执行一次。类似于其他支持面向对象编程(如 C# 和 Java )的语言中的静态构造函数。

一个类在其类体中可以有任意数量的 static {} 初始化块,静态字段一起被初始化,然后按照声明的顺序执行。我们可以使用static 块中的 super 属性来访问父类的属性。

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

However, we have to use an index of arr.length - N if we want to access the Nth item from the end of the array with square brackets.

但是, 如果我们想用方括号从数组末尾访问第N个元素必须使用 arr.length - N

const arr = ['a', 'b', 'c', 'd'];// 1st element from the end
console.log(arr[arr.length - 1]); // d// 2nd element from the end
console.log(arr[arr.length - 2]); // c

新的at() 方法让我们更简洁、更有表现力地完成这个任务。要访问数组末尾的 N 元素,只需向at() 传递一个负值 -N

const arr = ['a', 'b', 'c', 'd'];// 1st element from the end
console.log(arr.at(-1)); // d// 2nd element from the end
console.log(arr.at(-2)); // c

除了数组,字符串和TypedArray对象现在也有 at() 方法。Array.prototype.atString.prototype.at 用法基本一致。

TypedArray 指的是一类指定元素类型的数组,而不是实际的数组类型。(ES6+)
const str = 'Coding Beauty';
console.log(str.at(-1)); // y
console.log(str.at(-2)); // tconst typedArray = new Uint8Array([16, 32, 48, 64]);
console.log(typedArray.at(-1)); // 64
console.log(typedArray.at(-2)); // 48

8. 正则表达式匹配索引

这个新特性允许我们指定我们想要获得给定字符串中 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);

我们现在可以指定 d regex标志来获取匹配开始和结束的两个索引。

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 属性,其中包含起始和结束索引。

还可以添加命名组,如 ?<Z>

const re1 = /a+(?<Z>z)?/d;
const s1 = "xaaaz";
const m1 = re1.exec(s1);

console.log(m1.groups.Z);          // 'z'
console.log(m1.indices.groups.Z);  // [4, 5]

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;  
  // 重写方法 默认返回false
  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'));

解决这些问题的一种方法是在 Object.prototype.hasOwnProperty上调用call() 方法,像这样:

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

现在不需要这样做了,在ES13 可以使用新的内置的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)

Error 对象新增了一个 cause 属性,用于描述原始错误,有助于为错误添加额外的上下文信息,规范化整个错误抛出和收集。

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() 方法来查找数组中通过指定条件的元素。类似地,我们可以使用Array.findIndex() 来查找此元素的索引。虽然find() findIndex() 都从数组的第一个元素开始搜索,但在某些情况下,从最后一个元素开始搜索会更合适。

在某些情况下,从最后一个元素中查找可能会获得更好的性能。例如,这里我们使用find()findIndex()方法 使用条件value === 'y'来获取数组中的项:

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];
// 使用扩展语法复制整个数组,然后调用 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

这段代码更短,可读性更强。最重要的是,会返回正确结果。

结论

我们已经了解了ES13给JavaScript带来的最新特性。使用它们可以提高开发人员的工作效率,并以更简洁和清晰的方式编写更通俗易懂的代码。

来源: codingbeautydev.com

翻译:Gonwe

ECMAScript Javascript Javascript笔记 async/await ES13
Theme Jasmine by Kent Liao