ES2022(ES13)新特性解读

今天 ES2022 新特性来的比较早,考虑到有些特性或多或少已经提前体验过了。

这里讲的是截止至 2022 年 2 月 3 日已经到了 Finished Proposals,处于 stage 4 阶段提案。

Class Fields

Class Public Instance Fields 公共实例字段

在 ES6 的类中,我们想定义一个默认值,只能通过 constructor 里面定义:

class Counter {
  constructor() {
    this._num = 0;
  }
}

现在我们可以这样:

class Counter {
  _num = 0;
}

是不是感觉很熟悉?是的,我在 Node.js v12 就用了,但是现在才进到标准中。

当然也可以不初始化,默认就是 undefined

Private Instance Fields 私有实例字段

原来的类实例的所有字段都可以被访问和修改:

class Counter {
  _num = 0;
}

const counter = new Counter();
console.log(counter._num);      // 0

_num 是一些约定成俗表示私有的用法,但是并不能阻止该被访问和修改,防君子不防小人。

现在可以通过 # 前缀来表示私有,当我们访问或者修改时就会抛出错误:

class Counter {
  #num = 0;
}

const counter = new Counter();
console.log(counter.#num);      // Uncaught SyntaxError: Private field '#num' must be declared in an enclosing class

Private instance methods and accessors 私有实例方法和访问器

除了私有字段,方法和访问器同样可以通过 # 前缀来表示私有:

class Counter {
  #num;

  constructor() {
    console.log(this.#getNum)  // undefined
    this.#initNum = 0;
    console.log(this.#getNum)  // 0
  }

  get #getNum() {
    return this.#num
  }

  set #initNum(num) {
    this.#num = num;
  }
}

const counter = new Counter();
console.log(counter.#initNum);  // VM723:1 Uncaught SyntaxError: Private field '#initNum' must be declared in an enclosing class

Static class fields and methods 静态公共字段和方法

在新的提案中,我们可以往类添加静态字段和方法,使用 static 关键字声明,这在其他语言非常常见:

class Counter {
  #num = 0;

  static baseNum = 100;

  // 静态方法可以通过 this 访问静态字段
  static getDoubleBaseNum() {
    return this.baseNum * 2;
  }
}

// 静态字段和方法通过类本身访问
console.log(Counter.baseNum);             // 100
console.log(Counter.getDoubleBaseNum());  // 200

// 实例不能访问静态字段和方法
const counter = new Counter();
console.log(counter.baseNum);             // undefined

Private static class fields and methods 静态私有字段和方法

静态字段和方法也可以通过 # 前缀来表示私有:

class Counter {
  #num = 0;

  static #baseNum = 100;

  static getDoubleBaseNum() {
    return this.#baseNum * 2;
  }

  getBaseNum() {
    return Counter.#baseNum;
  }
}

// 私有静态字段不能被直接访问
console.log(Counter.#baseNum);            // Uncaught SyntaxError: Private field '#baseNum' must be declared in an enclosing class
// 同类静态方法可以访问私有静态字段
console.log(Counter.getDoubleBaseNum());  // 200

// 实例可以访问同类下的私有静态字段和方法
const counter = new Counter();
console.log(counter.getBaseNum());        // 100

Class Static Block 类静态初始化块

这个提案的也比较熟,Java 语言就有用到,先看个例子:

class Counter {
  static running;

  static {
    try {
      this.running = doRun();
    } catch {
      this.running = false;
    }
  }
}

从上面可以看出,static {} 很像静态的 constructor

它也可以访问修改私有静态字段和方法:

class Counter {
  static #baseNum = 100;

  static getDoubleBaseNum() {
    return this.#baseNum * 2;
  }

  static {
    this.#baseNum = 200;
  }
}

console.log(Counter.getDoubleBaseNum());  // 400

甚至将私有静态字段暴露出去:

let getBaseNum

class Counter {
  static #baseNum = 100;

  static {
    getBaseNum = () => this.#baseNum;
  }
}

console.log(getBaseNum());  // 100

Ergonomic brand checks for Private Fields 私有字段检查

主要是检测一个对象或实例是否存在私有字段或方法:

class C {
  #brand;

  #method() {}

  get #getter() {}

  static isC(obj) {
    return #brand in obj && #method in obj && #getter in obj;
  }
}

RegExp Match Indices

新提案允许我们利用 /d 标识符来表示想要匹配字符串的开始和结束索引。举个例子:

const re1 = /a+(z)?/;

const s1 = "xaaaz";
const m1 = re1.exec(s1);

console.log(m1[0]);  // 'aaaz'
console.log(m1[1]);  // 'z'

在此之前我们并不能完成知道所以匹配的字符在目标字符串的位置,现在通过 /d 标识符,匹配结果会多出一个属性 .indices

const re1 = /a+(z)?/d;

const s1 = "xaaaz";
const m1 = re1.exec(s1);

console.log(m1.indices[0]);               // [1, 5]
console.log(s1.slice(...m1.indices[0]));  // 'aaaz'
console.log(m1.indices[1]);               // [4, 5]
console.log(s1.slice(...m1.indices[1]));  // 'z'

还可以添加命名组,如 ?

const re1 = /a+(?z)?/d;

const s1 = "xaaaz";
const m1 = re1.exec(s1);

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

Top-level await

该提案可以让 await 提升到模块中,不需要和 async 强绑定了,在此之前:

// awaiting.mjs
let output;
async function main() {
  output = await new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(100)
    }, 500)
  });
}
main();
export { output };

如果我们要引用 output 值:

// usage.mjs
import { output } from "./awaiting.mjs";

console.log(output);                         // undefined
setTimeout(() => console.log(output, 1000);  // 100

很明显,我们不能立即引用到异步的值,所以大部分情况我们会引用一个返回异步调用的方法来解决问题,如:

// compiler.mjs
let vueCompiler
const getVueCompiler = async () => {
  if (vueCompiler) return vueCompiler
  vueCompiler = await import('@vue/compiler-sfc')
  return vueCompiler
}
export { getVueCompiler };

// usage.mjs
import { getVueCompiler } from "./compiler.mjs";

const compiler = await getVueCompiler()

在顶层 await 加持下,我们可以:

// awaiting.mjs
function main() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(100)
    }, 500)
  });
}
export const output = await main();

// usage.mjs
import { output } from "./awaiting.mjs";

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

可以看到, await 不需要在 async 函数中使用了,我们引用模块的可以等待 ESM 模块异步执行完毕在去执行。

.at()

我们要访问数组某一项,通常是这么做:

const arr = [1, 2, 3]

console.log(arr[0]);               // 1
console.log(arr[arr.length - 1]);  // 3

从上面可以看出,访问前面的数组还好,倒过来访问就略显难受,特别是数组动态算出来的,比如:

console.log([1, 2, 3 ...].map(v => v + 1)[[1, 2, 3 ...].map(v => v + 1).length - 1]);  // 4

上面这种情况,我们不得不用一个变量存起来。

新增 Array.prototype.at 就可以解决问题,这个跟 String.prototype.at 用法基本一致。

const arr = [1, 2, 3]
console.log(arr[arr.length - 1]);  // 3
// ↓↓
console.log(arr.at(-1));           // 3

// 动态算出来也能变得简洁
console.log([1, 2, 3 ...].map(v => v + 1)[[1, 2, 3 ...].map(v => v + 1).length - 1]);  // 4
// ↓↓
console.log([1, 2, 3 ...].map(v => v + 1).at(-1));                                     // 4

Accessible Object.prototype.hasOwnProperty

我记得最早的时候,我们要遍历一个对象,会这么写:

for (var k in obj) {
  if (obj.hasOwnProperty(k)) {
    // 获取可枚举对象
  }
}

后续这么使用 eslint 就会弹出提示:

Do not access Object.prototype method 'hasOwnProperty' from target object.

这是一个不安全的行为,比如 {"hasOwnProperty": 1},可能会导致服务器崩溃。

为了解决问题,我们改成这样:

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

这样就可以避免访问目标对象 Object 原型方法。

来到重点了,新的提案简化了:

Object.prototype.hasOwnProperty.call(obj, 'key')
// ↓↓
Object.hasOwn(obj, 'key')

Error Cause

举个例子:

async function getSolution() {
  const rawResource = await fetch('//domain/resource-a')
    .catch(err => {
      // 平时我们要抛出错误有以下几种方式:
      // 1. throw new Error('Download raw resource failed: ' + err.message);
      // 2. const wrapErr = new Error('Download raw resource failed');
      //    wrapErr.cause = err;
      //    throw wrapErr;
      // 3. 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

在新的提案中,加入了 cause 来收集原因,规范化整个错误抛出和收集:

async function doJob() {
  const rawResource = await fetch('//domain/resource-a')
    .catch(err => {
      // 抛出一个低等级错误err,可以通过 cause 包装成高等级错误 Error
      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

有了这个后,各种插件库的形形色色的错误类可以简单点了。

参考资料

TC39 ecma262 翻到最底下。

合集

  • ES2022(ES13)新特性解读
  • ES2021(ES12)新特性解读
  • ES2020(ES11)带来的新特性
  • ES2019(ES10)带来的 9 个新特性
  • ES2018(ES9)新特性
  • ES2017(ES8)新特性
  • ES2016(ES7)新特性

你可能感兴趣的:(ES2022(ES13)新特性解读)