目录

前言

正文

一、总览

二、详述

1、Top-level await(顶层 await)

2、Object.hasOwn()

3、at()

4、error.cause

5、正则表达式匹配索引

6、类

总结


前言

2022年6月22日,第123届 ECMA 大会批准了 ECMAScript 2022 语言规范,这意味着它现在正式成为标准。下面就来看看 ECMAScript 2022 有哪些新特性,是否有你了解过的。

正文

一、总览

1、Top-level await(顶层 await)

2、Object.hasOwn()

3、at()

4、error.cause

5、正则表达式匹配索引

6、类

二、详述

1、Top-level await(顶层 await)

在 ES2017 中,引入了 async await,以简化 Promise 的使用,但是 await 关键字只能在 async 函数内使用。如果在异步函数之外使用 await 就会报错。

顶层 await 允许我们在 async 函数外使用 await 关键字。它允许模块充当大型异步函数,通过顶层 await,这些模块可以等待资源加载,这样其他导入这些模块的模块在执行代码之前要等待资源加载完才会去执行。

之前由于 await 仅在 async 函数中使用,因此模块需通过将代码包装在 async 函数来在代码中包含await:

// test.js
let users;

export const fetchUsers = async () => {
  const res = await fetch('https://www.leoTest.com/users');
  users = res.json();
}
fetchUsers();

export { users };


// usingAwait.js
import { users } from './test.js';
console.log('users: ', users);
console.log('usingAwait module');

这样会有一个缺点,直接导入的 users 会是 undefined。如果我们需要访问得到它,需要在异步执行完成之后:

// usingAwait.js
import { users } from './test.js';

console.log('users:', users);   // 直接访问,undefined

setTimeout(() => {
  console.log('users:', users);   // 等待异步执行完成,再进行访问
}, 100);

console.log('usingAwait module');

当然,这种方法并不完全实现,因为如果异步函数执行花费的时间超过100毫秒,它就不会起作用了,users 仍然是 undefined。

还有另一个方法是导出一个 Promise,让导入模块知道数据已经准备好了:

// test.js
export default (async () => {                   // 导出一个 Promise
  const res = await fetch('https://www.leoTest.com/users');
  users = res.json();
})();
export { users };


// usingAwait.js
import Promise, { users } from './test.js';
Promise.then(() => { 
  console.log('usingAwait module');
  setTimeout(() => console.log('users:', users), 100); 
});

虽然这种方法似乎是会得出预期的结果,但也有一定的局限性:导入模块必须了解这种模式才能正确使用它。 

而 top-level await(顶层 await)可以解决这些问题:

// test.js
const res = await fetch('https://www.leoTest.com/users');   // 使用顶层 await
const users = res.json();
export { users };


// usingAwait.js
import { users } from './test.js';

console.log(users);
console.log('usingAwait module');

此时的 users 必须等到异步执行完毕才会访问。

顶层 await 在以下几种场景中将非常有用:

① 动态加载模块

const str = await import(`/i18n/${navigator.language}`);

② 资源初始化

const con = await dbConnector();

③ 依赖回退

let translations;
try {
  translations = await import('https://app.fr.json');
} catch {
  translations = await import('https://fallback.en.json');
}

2、Object.hasOwn()

在此之前,我们可以使用 Object.prototype.hasOwnProperty() 来检查一个属性是否属于该对象。

Object.hasOwn() 特性是一种更简洁、更可靠的检查属性是否直接设置在该对象上的方法:

const example = {
  name: 'leo'
};

console.log(Object.prototype.hasOwnProperty.call(example, 'name'));
// 更简介的方式
console.log(Object.hasOwn(example, 'name'));

3、at()

at() 是一个数组方法,用于通过给定索引来获取数组元素。当给定索引为正数时,这种新方法与使用中括号表示法访问具有相同的行为。当给出负整数索引时,就会从数组的最后一项开始检索:

const arr = [0,1,2,3,4,5];

console.log(arr[0]);   // 0
console.log(arr.at(0));   // 0

console.log(arr[arr.length - 1]);   // 5
console.log(arr.at(-1));   // 5

console.log(arr[arr.lenght - 2]);   // 4
console.log(arr.at(-2));   // 4

除了数组,字符串也同样适用:

const str = "hello world";

console.log(str[str.length - 1]);   // d
console.log(str.at(-1));   // d

4、error.cause

在 ES2022 规范中,new Error() 中可以指定导致它的原因:

function readFiles(filePaths) {
  return filePaths.map(
    (filePath) => {
      try {
        // ···
      } catch (error) {
        throw new Error(
          `While processing ${filePath}`,
          {cause: error}
        );
      }
  });
}

5、正则表达式匹配索引

该特性允许我们利用 d 字符来表示我们想要匹配字符串的开始和结束索引。以前,只能在字符串匹配操作期间获得一个包含提取的字符串和索引信息的数组。在某些情况下,这是不够的。因此,在这个规范中,如果设置标志 /d,将额外获得一个带有开始和结束索引的数组。

const matchObj = /(a+)(b+)/d.exec('aaaabb');

console.log(matchObj[1]);   // 'aaaa'
console.log(matchObj[2]);   // 'bb'

由于 /d 标识的存在,matchObj 还有一个属性 .indices,它用来记录捕获的每个编号组:

console.log(matchObj.indices[1]);   // [0, 4]
console.log(matchObj.indices[2]);   // [4, 6]

6、类

① 公共实例字段

公共类字段允许我们使用赋值运算符(=)将实例属性添加到类定义中。下面是一个计数器的例子:

import React, { Component } from "react";

export class Incrementor extends Component {
  constructor() {
    super();
    this.state = {
      count: 0,
    };
    this.increment = this.increment.bind(this);
  }

  increment() {
    this.setState({ count: this.state.count + 1 });
  }

  render() {
    return (
      <button onClick={this.increment}>Increment: {this.state.count}</button>
    );
  }
}

在这个例子中,在构造函数中定义了实例字段和绑定方法,通过新的类语法,可以使代码更加直观。新的公共类字段语法允许我们直接将实例属性作为属性添加到类上,而无需使用构造函数方法。这样就简化了类的定义,使代码更加简洁、可读:

import React from "react";

export class Incrementor extends React.Component {
  state = { count: 0 };

  increment = () => this.setState({ count: this.state.count + 1 });

  render = () => (
    <button onClick={this.increment}>Increment: {this.state.count}</button>
  );
}

有些小伙伴可能已经发现了,这个功能很早就可以使用了呀。但是它现在还不是标准的 ECMAScript,默认是不开启的,如果使用 create-react-app 创建 react 项目,那么它默认是启用的,否则我们必须使用正确的 babel 插件才能正常使用(@babel/preset-env)。

以下是一些公共实例字段的注意事项:

⑴ 公共实例字段存在于每个创建的类实例上。它们要么是在 Object.defineProperty() 中添加,要么是在基类中的构造时添加,要么在子类的 super() 返回之后添加

class Incrementor {
  count = 0
}

const instance = new Incrementor();
console.log(instance.count);   // 0

⑵ 未初始化的字段会自动设置为 undefined

class Incrementor {
  count
}

const instance = new Incrementor();
console.log(instance.count);   // undefined

⑶ 可以进行字段的计算

const PREFIX = 'main';

class Incrementor {
  [`${PREFIX}Count`] = 0
}

const instance = new Incrementor();
console.log(instance.mainCount);   // 0

② 私有实例字段、方法和访问器

默认情况下,ES6 中所有属性都是公共的,可以在类外检查或修改:

class TimeTracker {
  name = 'leo';
  project = 'blog';
  hours = 0;

  set addHours(hour) {
    this.hours += hour;
  }

  get timeSheet() {
    return `${this.name} works ${this.hours || 'nothing'} hours on ${this.project}`;
  }
}

let person = new TimeTracker();
person.addHours = 2;   // 标准 setter
person.hours = 4;   // 绕过 setter 进行设置
person.timeSheet;   // 'leo works 4 hours on blog'

可以看到,在类中没有任何措施可以防止在不调用 setter 的情况下更改属性。

而私有类字段将使用 # 前缀定义,在上面的示例中,可以修改它以包含私有类字段,以防止在类方法之外更改属性:

class TimeTracker {
  name = 'leo';
  project = 'blog';
  #hours = 0;   // 私有类字段

  set addHours(hour) {
    this.#hours += hour;
  }

  get timeSheet() {
    return `${this.name} works ${this.#hours || 'nothing'} hours on ${this.project}`;
  }
}

let person = new TimeTracker();
person.addHours = 4; // 标准 setter
person.timeSheet     // 'leo works 4 hours on blog'

当尝试在 setter 方法之外修改私有类字段时,就会报错:

person.hours = 4;   // Error Private field '#hours' must be declared in an enclosing class

还可以将方法或 getter/setter 设为私有,只需要给这些方法名称前面加 # 即可:

class TimeTracker {
  name = 'leo';
  project = 'blog';
  #hours = 0;   // 私有类字段

  set #addHours(hour) {
    this.#hours += hour;
  }

  get #timeSheet() {
    return `${this.name} works ${this.#hours || 'nothing'} hours on ${this.project}`;
  }

  constructor(hours) {
    this.#addHours = hours;
    console.log(this.#timeSheet);
  }
}

let person = new TimeTracker(4);   // 'leo works 4 hours on blog'

③ 静态公共字段

在 ES6 中,不能在类的每个实例中访问静态字段或方法,只能在原型中访问。ES2022 提供了一种在 JavaScript 中使用 static 关键字声明静态类字段的方法:

class Shape {
  static color = 'blue';

  static getColor() {
    return this.color;
  }

  getMessage() {
    return `color:${this.color}` ;
  }
}

可以从类本身访问静态字段和方法:

console.log(Shape.color);   // blue
 console.log(Shape.getColor());   // blue

 console.log('color' in Shape);   // true
 console.log('getColor' in Shape);   // true
 console.log('getMessage' in Shape);   // false

实例不能访问静态字段和方法:

const shapeInstance = new Shape();
console.log(shapeInstance.color);   // undefined
console.log(shapeInstance.getColor);   // undefined
console.log(shapeInstance.getMessage());   // color:undefined

静态字段只能通过静态方法访问:

console.log(Shape.getColor());   // blue
console.log(Shape.getMessage());   //TypeError: Shape.getMessage is not a function

这里的 Shape.getMessage() 就报错了,因为 getMessage 不是一个静态函数,所以它不能通过类名 Shape 访问。可以通过以下方式来解决这个问题:

getMessage() {
  return `color:${Shape.color}` ;
}

静态字段和方法是从父类继承的:

class Rectangle extends Shape { }   // 继承

console.log(Rectangle.color);   // blue
console.log(Rectangle.getColor());   // blue
console.log('color' in Rectangle);   // true
console.log('getColor' in Rectangle);   // true
console.log('getMessage' in Rectangle);   // false

④ 静态私有字段和方法

与私有实例字段和方法一样,静态私有字段和方法也使用哈希 # 前缀来定义:

class Shape {
  static #color = 'blue';

  static #getColor() {
    return this.#color;
  }

  getMessage() {
    return `color:${Shape.#getColor()}` ;
  }
}
const shapeInstance = new Shape();
shapeInstance.getMessage();   // color:blue

私有静态字段有一个限制,即只有定义私有静态字段的类才能访问该字段。这可能在使用 this 时导致出乎意料的情况:

class Shape {
  static #color = 'blue';
  static #getColor() {
    return this.#color;
  }
  static getMessage() {
    return `color:${this.#color}` ;
  }
  getMessageNonStatic() {
    return `color:${this.#getColor()}` ;
  }
}

class Rectangle extends Shape {}

console.log(Rectangle.getMessage());   // Uncaught TypeError: Cannot read private member #color from an object whose class did not declare it

const rectangle = new Rectangle();
console.log(rectangle.getMessageNonStatic());   // TypeError: Cannot read private member #getColor from an object whose class did not declare it

在这个例子中,this 指向的是 Rectangle 类,它无权访问私有字段 #color。当我们尝试调用 Rectangle.getMessage() 时,它无法读取 #color 并抛出了 TypeError。可以这样来进行修改:

class Shape {
  static #color = 'blue';
  static #getColor() {
    return this.#color;
  }
  static getMessage() {
    return `${Shape.#color}`;
  }
  getMessageNonStatic() {
    return `color:${Shape.#getColor()} color`;
  }
}

class Rectangle extends Shape {}
console.log(Rectangle.getMessage()); // color:blue

const rectangle = new Rectangle();
console.log(rectangle.getMessageNonStatic()); // color:blue

总结

以上就是 ES2022 发布的几个新特性,有你了解过的吗?

如文章有不恰当之处,请不吝赐教。