今天 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)新特性