ES13(ES2022)新特性

4 阅读4分钟

发布时间:2022年6月 ES13 新增了顶层 await、类私有字段、数组新方法等特性,是近年来改动较大的版本。


1. 顶层 Await(Top-level Await)

在 ES Module 中,允许在模块顶层直接使用 await,无需包裹在 async 函数中:

基本用法

// config.js
const response = await fetch('/api/config');
export const config = await response.json();

// 直接在顶层 await
const data = await loadData();
console.log(data);

动态依赖加载

// 根据条件加载不同的模块
let strings;
if (isChinese) {
  strings = await import('./zh-CN.js');
} else {
  strings = await import('./en-US.js');
}

初始化

// 数据库连接初始化
const db = await connectDB();
export { db };

// 资源预加载
const fonts = await Promise.all([
  loadFont('/fonts/a.woff2'),
  loadFont('/fonts/b.woff2')
]);

注意

  • 只能在 ES Module 中使用(<script type="module">.mjs 文件)
  • CommonJS 模块不支持
  • 顶层 await 会阻塞模块的求值

2. 类私有字段和私有方法

使用 # 前缀定义真正的私有成员:

私有字段

class Person {
  #name;  // 私有字段(必须先声明)
  
  constructor(name) {
    this.#name = name;
  }
  
  getName() {
    return this.#name;
  }
}

let p = new Person('张三');
console.log(p.getName());  // '张三'
console.log(p.#name);      // SyntaxError:私有字段不能在类外访问
console.log('name' in p);  // false:私有字段不会出现在 in 操作中

私有方法

class Calculator {
  #validate(num) {
    if (typeof num !== 'number') throw new Error('必须是数字');
    return num;
  }
  
  add(a, b) {
    this.#validate(a);
    this.#validate(b);
    return a + b;
  }
}

let calc = new Calculator();
calc.add(1, 2);       // 3
calc.#validate(1);    // SyntaxError:私有方法不能在类外调用

私有静态字段和方法

class MyClass {
  static #count = 0;
  
  static getInstance() {
    MyClass.#count++;
    return new MyClass();
  }
  
  static getCount() {
    return MyClass.#count;
  }
}

console.log(MyClass.getCount());  // 0
MyClass.getInstance();
MyClass.getInstance();
console.log(MyClass.getCount());  // 2

私有字段的 in 检查

class Dog {
  #bark = true;
  
  isBarking() {
    return #bark in this;  // true
  }
}

对比旧方式

// 旧方式:约定用 _ 表示私有(但实际可以外部访问)
class OldStyle {
  constructor() {
    this._private = '可以访问';
  }
}

// 新方式:真正的私有(外部完全不可访问)
class NewStyle {
  #private = '真正私有';
}

3. 类静态初始化块(Static Initialization Block)

在类中使用 static { } 块执行初始化代码:

基本用法

class MyClass {
  static count;
  static max;
  
  static {
    // 初始化静态字段
    MyClass.count = 0;
    MyClass.max = 100;
    
    // 可以使用 try...catch
    try {
      MyClass.config = JSON.parse(fs.readFileSync('config.json'));
    } catch {
      MyClass.config = { default: true };
    }
  }
}

多个静态块

class Config {
  static db;
  static cache;
  
  static {
    Config.db = connectDB();
  }
  
  static {
    Config.cache = new Map();
  }
}

访问私有字段

class DB {
  static #connection;
  
  static {
    DB.#connection = createConnection();
  }
  
  static getConnection() {
    return DB.#connection;
  }
}

4. Array.prototype.at()

通过索引访问数组元素,支持负数索引:

let arr = [1, 2, 3, 4, 5];

arr.at(0);    // 1(第一个)
arr.at(2);    // 3
arr.at(-1);   // 5(最后一个)
arr.at(-2);   // 4(倒数第二个)
arr.at(10);   // undefined(超出范围)

// 对比旧写法
arr[arr.length - 1];  // 5(旧方式取最后一个)
arr.at(-1);           // 5(新方式,更简洁)

也适用于字符串

let str = 'hello';
str.at(0);    // 'h'
str.at(-1);   // 'o'

也适用于 TypedArray

let typed = new Int8Array([10, 20, 30]);
typed.at(-1);  // 30

5. Object.hasOwn()

更安全地检查对象自身是否拥有某个属性:

let obj = { name: '张三' };

// 旧方式
obj.hasOwnProperty('name');  // true
// 问题1:如果对象没有 hasOwnProperty 方法(Object.create(null))会报错
// 问题2:如果属性名就是 hasOwnProperty,会覆盖

// 新方式
Object.hasOwn(obj, 'name');   // true
Object.hasOwn(obj, 'age');    // false

// 安全处理没有原型的对象
let safe = Object.create(null);
safe.key = 'value';
Object.hasOwn(safe, 'key');   // true
// safe.hasOwnProperty('key');  // TypeError!

对比 in 操作符

// in 会检查原型链
'name' in obj;         // true
'toString' in obj;     // true(继承的)

// Object.hasOwn 只检查自身
Object.hasOwn(obj, 'name');      // true
Object.hasOwn(obj, 'toString');  // false

6. RegExp 的 d 标志(Match Indices)

正则匹配时返回匹配的起始和结束索引:

let re = /test(?<year>\d{4})-(?<month>\d{2})/d;
let match = re.exec('test2023-12');

match.indices;                    // [[0, 12], [4, 8], [9, 11]]
match.indices.groups;             // { year: [4, 8], month: [9, 11] }
match.indices.groups.year;        // [4, 8]
match.indices.groups.month;       // [9, 11]

// 应用:高亮匹配文本
let text = 'test2023-12';
let [start, end] = match.indices[0];
let highlighted = text.slice(0, start) 
  + '<mark>' + text.slice(start, end) + '</mark>' 
  + text.slice(end);
// '<mark>test2023-12</mark>'

7. Error 对象的 cause 属性

为错误添加原因链:

try {
  try {
    JSON.parse('invalid');
  } catch (parseErr) {
    throw new Error('数据解析失败', { cause: parseErr });
  }
} catch (err) {
  console.log(err.message);     // '数据解析失败'
  console.log(err.cause);       // 原始的 JSON 解析错误
  console.log(err.cause.message); // 'Unexpected token...'
}

自定义错误

class ValidationError extends Error {
  constructor(message, field, cause) {
    super(message, { cause });
    this.field = field;
  }
}

try {
  throw new ValidationError('年龄必须是数字', 'age', new TypeError('expected number'));
} catch (err) {
  console.log(err.field);  // 'age'
  console.log(err.cause);  // TypeError: expected number
}

8. .at() 方法的其他对象支持

at() 方法不仅适用于数组,还适用于:

String

'hello'.at(0);   // 'h'
'hello'.at(-1);  // 'o'

TypedArray

new Uint8Array([1, 2, 3]).at(-1);  // 3

总结

特性说明重要性
顶层 Await模块顶层直接使用 await⭐⭐⭐⭐⭐
私有字段 #类的真正私有成员⭐⭐⭐⭐⭐
私有方法 #类的真正私有方法⭐⭐⭐⭐⭐
静态初始化块类的静态初始化代码⭐⭐⭐⭐
Array.at()支持负数索引访问⭐⭐⭐⭐
Object.hasOwn()安全检查自身属性⭐⭐⭐⭐
正则 d 标志匹配索引⭐⭐⭐
Error.cause错误原因链⭐⭐⭐