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'

还可以添加命名组,如 ?<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]

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 翻到最底下。

合集

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,384评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,845评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,148评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,640评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,731评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,712评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,703评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,473评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,915评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,227评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,384评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,063评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,706评论 3 324
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,302评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,531评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,321评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,248评论 2 352

推荐阅读更多精彩内容