ES13的11个超赞的新属性

23,393 阅读11分钟

声明:本文为翻译文章,原文为11 Amazing New JavaScript Features in ES13

像其他语言一样,JavaScript也在不断迭代和进化。JS每年都会加入很多新的功能来让自己变得越发强大,也正是这样,我们开发者才能写出更加表意和准确的代码。

在这篇文章中我们会通过一些例子来看一下最新的ECMAScript 2022(ES13)给我们开发者带来的11个超赞的新功能。

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. 给类定义私有方法和成员变量

ES13之前,我们是不可能给类定义私有成员的。所以一些程序员为了表示某个成员变量是一个私有属性,会给该属性名添加一个下划线(_)作为后缀。可是这只能作为程序员之间的君子之约来使用,因为这些变量其实还是可以被外界访问到的。

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

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

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

// 这些所谓的私有属性其实还是可以被外界访问到的
console.log(person._firstName); // Joseph
console.log(person._lastName); // Stevens

// 而且还能被改来改去
person._firstName = 'Robert';
person._lastName = 'Becker';

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

不过ES13出来后,妈妈再也不用怕我们的私有属性会被别人访问和更改了。在ES13中,我们只需要给我们的属性名添加一个hashtag(#)前缀,这个属性就变成私有的了。当我们的属性变为私有后,任何外界对其的访问都会出错哦。

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

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

这里值得一提的是,上面说的SyntaxError是一个编译时抛出的错误,所以你不会等你的代码运行后才知道这个属性被非法访问了。

3. 支持在最外层写await

我们都知道在JS中,await操作符的作用就是当我们碰到一个promise的时候,我们可以使用await来暂停当前代码的执行,等到这个promise被settled(fulfilled或者rejected)了,我们才继续当前代码的执行。

可是之前使用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等面向对象的编程语言的静态构造函数的用法。

一个类可以定义任意多的静态代码块,这些代码块会和穿插在它们之间的静态成员变量一起按照定义的顺序在类初始化的时候执行一次。我们还可以使用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来判断某个对象是否拥有某个私有属性

这个新属性的名字其实叫做Ergonomic Brand Checks for Private Fields,原谅我才疏学浅,我实在不知道怎么翻译,所以大概将它的作用表达了出来。总的来说,它就是用来判断某个对象是否拥有某个特定的私有属性,是通过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个元素,我们会使用方括号[N - 1]:

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

这样写大多数时候都是没有什么问题的,其他语言也是这么做的,可是当我们需要访问数组倒数第N个元素的时候,我们的代码就变得有点奇怪了:

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

// 倒数第一个元素
console.log(arr[arr.length - 1]); // d

// 倒数第二个元素
console.log(arr[arr.length - 2]); // c

这么看你的代码是不是一下子变丑了?不用怕,ES13的at()函数帮你写出更优雅的代码!使用新的new()方法,当我们想要访问倒数第N个元素时,我们只需要传入-Nat()即可:

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

// 倒数第一个元素
console.log(arr.at(-1)); // d
// 倒数第二个元素
console.log(arr.at(-2)); // c

你看,你的代码是不是一下子表意多了!除了数组,stringTypedArray对象也支持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在返回match对象的时候,给我们返回匹配到的子字符串的开始和结束索引。

ES13之前,我们只能获取正则表达式匹配到的子字符串的开始索引:

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

ES13后,我们就可以给正则表达式添加一个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()方法

在JS中,我们可以使用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

上面的写法第二个问题就是:当一个对象是通过Object.create(null)创建出来的具有null原型的对象时,你想在这个对象上面调用hasOwnProperty这个方法是会报错的:

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这个Functioncall方法:

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

Object.prototype.hasOwnProperty.call(obj, 'color')

hasOwnProperty需要被多次调用的时候,我们可以通过将这部分逻辑抽象成一个方法来减少重复的代码:

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属性

ES13后,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. 数组支持逆序查找

在JS中,我们可以使用数组的find()函数来在数组中找到第一个满足某个条件的元素。同样地,我们还可以通过findIndex()函数来返回这个元素的位置。可是,无论是find()还是findIndex(),它们都是从数组的头部开始查找元素的,可是在某些情况下,我们可能有从数组后面开始查找某个元素的需要。例如我们知道待查找的元素在比较靠后的位置,从后面开始寻找的话会有更好的性能,就像下面这个例子:

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

// 我们想要找的y元素比较靠后, 顺序查找性能不好
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

在这种情况下使用find()findIndex()也可以,就是性能差点而已。ES13出来后,我们终于有办法处理这种情况了,那就是使用新的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

另外一种使用findLast()findLastIndex()的场景就是我们本身就是想要寻找最后一个满足某个条件的元素,例如找到数组里面最后一个偶数,这个时候还用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()来反转数组,然后再调用find()findIndex()函数。不过这个做法有两个问题,一个问题就是需要改变原数组,这个问题可以通过拷贝数组来解决,占用空间多点而已。另一个问题是findIndex()得到索引后我们还要做一些额外的计算才能得到元素原数组的位置,具体做法是:

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

// 在调用reverse之前先拷贝函数,以防改变原数组
const reversed = [...nums].reverse();
// 这次返回对了,是10
const lastEven = reversed.find((value) => value % 2 === 0);
// findIndex得到的结果还是不对的
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最新的11个属性。作为一个开发者,我们可以使用它们来提高自己的生产效率和编写更加简洁和表意的代码,你还不赶紧在项目里面实践一把?

个人技术动态

关注我的公众号 - 进击的大葱获取我的最新技术推送

wechat.jpeg