ES2022新特性一览

7,930 阅读6分钟

ES2022新特性共有8条,并且当前全部处于 stage 4 阶段,预计将在今年六月成为标准。下面我们一起先睹为快!

1. Class Fields

Field declarations

在ES2022之前,给class定义一个字段,我们要在constructor里定义:

class X {
  constructor() {
    this.a = 123;
  }
}

ES2022允许我们直接这么写:

class X {
  a = 123;
}

Fields without initializers are set to undefined

公共字段和私有字段如果没有被初始化赋值,则会默认设置为undefined

class X {
  a;
  #b;
  getB() {
      console.log(this.#b);
  }
}
x = new X();
x.a // undefined
x.getB() // undefined

对于多个class继承,如果一个字段在多个class上都有定义,那么会以最近的class定义为准

class A {
  a = 1;
}
class B extends A {
  a;
}
const b = new B();
b.a // undefined


class C {
  a;
}
class D extends C {
  a = 1;
}
const d = new D();
d.a // 1

Public fields created with Object.defineProperty

公共字段都是通过Object.defineProperty创造的。当某一个字段,get、set也同时存在时,TC39委员会经过漫长的讨论,最终决定用Object.defineProperty的get、set默认行为,而不是class里定义的get和set。

class A {
  set x(value) { console.log(++value); }
  get x() { return 'x' }
}
class B extends A {
  x = 1;
}
const b = new B();
b.x; // 1 (并不会返回'x')
b.x = 2; // 控制台不会打印3

Private fields

在ES2022之前,并没有实际意义上的私有字段。大家形成一种默契,通常用下划线_开头的字段名来表示私有字段,但是这些字段还是可以手动更改的。

ES2022给我们提供了更加安全便捷的私有字段定义方法,就是以#开头命名的字段,都会被当成私有字段,在class外部是没办法直接读取、修改这些私有字段的。

class X {
  #a = 123;
}
const x = new X();
x.#a // Uncaught SyntaxError: Private field '#a' must be declared in an enclosing class

2. RegExp Match Indices

正则表达式增加了一个/d修饰符,当使用正则表达式的exec()方法时,如果有/d修饰符,那么结果会多返回一个indices属性,用来表示匹配的结果的在原字符串中的起始index值。

const re1 = /a+/d;

const s1 = "aaabbb";
const m1 = re1.exec(s1);
m1.indices[0] //[0, 3];

控制台打印结果 bd8510f9aaa96686ac143ffca8e8e6b2.png

如果正则表达式中有具名捕获组,那么indices[1]则表示捕获组的起始index值,indices.groups同样记录了捕获组信息

const re1 = /a+(?<B>b+)/d;

const s1 = "aaabbbccc";
const m1 = re1.exec(s1);
m1.indices[1] //[3, 6];
m1.indices.groups //{ B: [3, 6] };

3. Top-level await

之前我们使用await时,必须使用async包裹起来,新的提案允许我们直接使用await; 官方的例子:

// awaiting.mjs
import { process } from "./some-module.mjs";
export default (async () => {
  const dynamic = await import(computedModuleSpecifier);
  const data = await fetch(url);
  const output = process(dynamic.default, data);
  return { output };
})();
// usage.mjs
import promise from "./awaiting.mjs";

export default promise.then(({output}) => {
  function outputPlusValue(value) { return output + value }

  console.log(outputPlusValue(100));
  setTimeout(() => console.log(outputPlusValue(100), 1000);

  return { outputPlusValue };
});

不使用顶层await,awaiting.mjs export的是一个异步方法,其他文件引用后,代码逻辑要写到回调里。

// awaiting.mjs
import { process } from "./some-module.mjs";
const dynamic = import(computedModuleSpecifier);
const data = fetch(url);
export const output = process((await dynamic).default, await data);
// usage.mjs
import { output } from "./awaiting.mjs";
export function outputPlusValue(value) { return output + value }

console.log(outputPlusValue(100));
setTimeout(() => console.log(outputPlusValue(100), 1000);

使用顶层await,awaiting.mjs export的是最终结果,其他文件引用后就可以直接使用。

4. Ergonomic brand checks for Private Fields

私有字段检测,之前在判断一个对象里有没有某个私有字段,是比较麻烦的,因为在访问对象上一个不存在的私有属性时,会抛出异常。通常用try/catch确保方法不会报错。

class C {
  #brand;
  
  static hasBrand(obj) {
    try {
      obj.#brand;
      return true;
    } catch {
      return false;
    }
  }
}
const c = new C();
const d = {};
C.hasBrand(c) // true
C.hasBrand(d) // false

ES2022新提案中,可以用in操作符来判断对象中是否存在某个私有字段、私有方法或者getter。

class C {
  #brand;

  #method() {}

  get #getter() {}

  static isC(obj) {
    return #brand in obj && #method in obj && #getter in obj;
  }
}
const c = new C();
const d = {};
C.isC(c) // true
C.isC(d) // false

5. .at()

新增的取值方法,可作用于Array, String, TypedArray

.at()接收一个参数,对于数组array=[1, 2, 3]

  • 当参数是正数n时,结果跟直接获取数组的第n个元素array[n]一样
  • 当参数是负数-n时,相当于倒取第n个元素,等同于array[-n + array.length]
  • 当参数是其它值或者空时,直接返回数组第一个元素
const arr = [1, 2, 3, 4, 5];
arr.at(1) // 2
arr.at(-1) // 5
arr.at(-10) // undefined
arr.at('aaaa') // 1
arr.at() // 1

6. Accessible Object.prototype.hasOwnProperty

ES2022在Object原型上新增了Object.hasOwn方法,判断对象本身是否存在某个属性,是Object.prototype.hasOwnProperty的改良版本。

// object继承自对象{ foo: 'foo' }
const object = Object.create({ foo: 'foo' })
// 再给本身添加属性bar
object.bar = 'bar'

object.hasOwnProperty('foo') // false
object.hasOwnProperty('bar') // true

Object.hasOwn(object, 'foo') // false
Object.hasOwn(object, 'bar') // true

当使用Object.create(null) 创建一个没有继承 Object.prototype 的对象时,hasOwnProperty 方法无法直接使用;

const object = Object.create(null)
object.hasOwnProperty("foo") // Uncaught TypeError: Object.create(...).hasOwnProperty is not a function
Object.prototype.hasOwnProperty.call(object, 'foo') // false
Object.hasOwn(object, 'foo') // false

7. Class Static Block

以前,我们初始化类的静态变量只能在定义类的时候去做,而对于一些无法显示声明的变量,我们往往使用一个函数去初始化。我们也不能放到构造函数里面,因为在构造函数中,只能初始化具体某一实例的变量。

现在,我们可以在类内部开辟一个专门为静态成员初始化的代码块,我们称之为“静态代码块”

const doSomethingWith = (x) => {
    return {y: x++, z: x**2}
}
// without static blocks:
class C {
  static x = 3;
  static y;
  static z;
}

try {
  const obj = doSomethingWith(C.x);
  C.y = obj.y
  C.z = obj.z;
}
catch {
  C.y = 0;
  C.z = 0;
}

// with static blocks:
class C {
  static x = 3;
  static y;
  static z;
  static {
    try {
      const obj = doSomethingWith(this.x);
      this.y = obj.y;
      this.z = obj.z;
    }
    catch {
      this.y = 0;
      this.z = 0;
    }
  }
}

静态块提供了对私有属性的特权访问权限

let getX;

class C {
  #x
  constructor(x) {
    this.#x = x;
  }

  static {
    // getX has privileged access to #x
    getX = (obj) => obj.#x;
  }
}

const c = new C('I am X');
getX(c) // I am X

8. Error Cause

之前,我们在封装错误信息时,比较繁琐,没有统一的字段的表示错误原因

async function doJob() {
  const rawResource = await fetch('//domain/resource-a')
    .catch(err => {
      // How to wrap the error properly?
      // 1. 字符串拼接错误原因
      //    throw new Error('Download raw resource failed: ' + err.message);
      // 2. 用cause字段承接报错信息
      //    const wrapErr = new Error('Download raw resource failed');
      //    wrapErr.cause = err;
      //    throw wrapErr;
      // 3. 二次封装Error
      //    class CustomError extends Error {
      //      constructor(msg, cause) {
      //        super(msg);
      //        this.cause = cause;
      //      }
      //    }
      //    throw new CustomError('Download raw resource failed', err);
    })
  const jobResult = doComputationalHeavyJob(rawResource);
  await fetch('//domain/upload', { method: 'POST', body: jobResult });
}

await doJob(); // => TypeError: Failed to fetch

新提案在Error构造函数新增了一个可选参数cause,允许我们在实例化Error时,将错误原因以参数形式传入,省去了我们自己单独处理的成本。

async function doJob() {
  const rawResource = await fetch('//domain/resource-a')
    .catch(err => {
      throw new Error('Download raw resource failed', { cause: err });
    });
  const jobResult = doComputationalHeavyJob(rawResource);
  await fetch('//domain/upload', { method: 'POST', body: jobResult })
    .catch(err => {
      throw new Error('Upload job result failed', { cause: err });
    });
}

try {
  await doJob();
} catch (e) {
  console.log(e);
  console.log('Caused by', e.cause);
}
// Error: Upload job result failed
// Caused by TypeError: Failed to fetch

参考资料

  1. github.com/tc39/propos…
  2. ES2022 新特性必知必会