Skip to content

ECMAScript 2022(ES13)

Class Public Instance Fields

公共实例字段允许在类中直接声明属性,无需在构造函数中初始化。

typescript
{
  class ES13Class {
    name = 'wudi'
    age: unknown
    sex: unknown
  }

  const es13class = new ES13Class()

  console.log(es13class.name) // 'wudi'
  console.log(es13class.age) // undefined
  console.log(es13class.sex) // undefined
}

Private Instance Fields

使用 # 前缀声明私有字段,只能在类内部访问。

typescript
{
  class ES13Class {
    #name: string // 私有字段
    #age = 18

    constructor(name: string) {
      this.#name = name
    }

    getName() {
      return this.#name
    }

    getAge() {
      return this.#age
    }
  }

  const es13class = new ES13Class('wudi')

  console.log(es13class.getName()) // 'wudi'
  console.log(es13class.getAge()) // 18
  
  // console.log(es13class.#name) // SyntaxError: Private field '#name' must be declared in an enclosing class
}

Private Methods and Accessors

私有方法和访问器只能在类内部调用。

typescript
{
  class ES13Class {
    #name: string

    constructor(name: string) {
      this.#name = name
    }

    // 私有方法
    #formatName() {
      return `My name is: ${this.#name}`
    }

    // 私有 getter
    get #privateAge() {
      return 18
    }

    // 私有 setter
    set #privateName(value: string) {
      this.#name = value
    }

    getFormattedName() {
      return this.#formatName()
    }
  }

  const es13class = new ES13Class('wudi')
  console.log(es13class.getFormattedName()) // 'My name is: wudi'
  
  // console.log(es13class.#formatName()) // SyntaxError: Private field '#formatName' must be declared in an enclosing class
}

Static Class Fields and Methods

静态字段和方法属于类本身,而不是实例。

typescript
{
  class ES13Class {
    static baseNum = 100
    static #privateStaticNum = 200

    // 静态方法
    static getBaseNum() {
      return this.baseNum
    }

    // 静态方法访问私有静态字段
    static getPrivateNum() {
      return this.#privateStaticNum
    }

    // 静态箭头函数
    static doubleBaseNum = () => this.baseNum * 2
  }

  console.log(ES13Class.baseNum) // 100
  console.log(ES13Class.getBaseNum()) // 100
  console.log(ES13Class.getPrivateNum()) // 200
  console.log(ES13Class.doubleBaseNum()) // 200

  // 实例无法访问静态成员
  const instance = new ES13Class()
  // console.log(instance.baseNum) // undefined
}

Class Static Block

类静态初始化块在类定义时执行,用于初始化静态字段或执行静态初始化逻辑。

typescript
{
  let getPrivateData: (obj: any) => any
  
  class ES13Class {
    static config = 'default'
    static #privateData = []

    #instanceData

    constructor(data: any) {
      this.#instanceData = { data }
    }

    static {
      // 静态初始化块
      console.log('Class is being initialized')
      this.#privateData = ['item1', 'item2']
      
      // 导出访问私有数据的函数
      getPrivateData = (obj) => {
        if (!(obj instanceof ES13Class)) {
          throw new Error('Invalid object')
        }
        return obj.#instanceData
      }
    }

    static {
      // 可以有多个静态块
      this.config = 'initialized'
    }
  }

  console.log(ES13Class.config) // 'initialized'
  
  const instance = new ES13Class({ value: 42 })
  console.log(getPrivateData(instance)) // { data: { value: 42 } }
}

Ergonomic Brand Checks for Private Fields

使用 in 操作符检查对象是否包含特定的私有字段。

typescript
{
  class ES13Class {
    #brand = 'ES13'

    static hasValidBrand(obj: any) {
      // 检查对象是否包含私有字段 #brand
      return #brand in obj
    }

    static getBrand(obj: any) {
      if (!(#brand in obj)) {
        throw new Error('Object does not have valid brand')
      }
      return obj.#brand
    }
  }

  const validInstance = new ES13Class()
  const invalidObject = {}

  console.log(ES13Class.hasValidBrand(validInstance)) // true
  console.log(ES13Class.hasValidBrand(invalidObject)) // false

  console.log(ES13Class.getBrand(validInstance)) // 'ES13'
  // console.log(ES13Class.getBrand(invalidObject)) // Error: Object does not have valid brand
}

Object.hasOwn()

Object.hasOwn() 作为 Object.prototype.hasOwnProperty() 的更安全替代方案。

typescript
{
  const obj = {
    name: 'wudi',
    age: 18
  }

  // 传统方式
  console.log(Object.prototype.hasOwnProperty.call(obj, 'name')) // true

  // 新方式 - 更简洁安全
  console.log(Object.hasOwn(obj, 'name')) // true
  console.log(Object.hasOwn(obj, 'height')) // false

  // 处理 null 原型对象
  const nullObj = Object.create(null)
  nullObj.prop = 'value'
  
  // console.log(nullObj.hasOwnProperty('prop')) // TypeError: nullObj.hasOwnProperty is not a function
  console.log(Object.hasOwn(nullObj, 'prop')) // true
}

Array.prototype.at()

使用 at() 方法进行反向索引,支持负数索引。

typescript
{
  const arr = ['a', 'b', 'c', 'd', 'e']

  // 传统方式获取最后一个元素
  console.log(arr[arr.length - 1]) // 'e'
  
  // 使用 at() 方法
  console.log(arr.at(-1)) // 'e'
  console.log(arr.at(-2)) // 'd'
  console.log(arr.at(0)) // 'a'
  console.log(arr.at(1)) // 'b'

  // 超出范围返回 undefined
  console.log(arr.at(10)) // undefined
  console.log(arr.at(-10)) // undefined

  // 链式调用中的便利性
  const result = [1, 2, 3]
    .map(x => x * 2)
    .filter(x => x > 2)
    .at(-1) // 获取最后一个元素
  
  console.log(result) // 6
}

Error Cause

为 Error 对象添加 cause 属性,用于错误链追踪。

typescript
{
  function parseData(data: string) {
    try {
      return JSON.parse(data)
    } catch (err) {
      throw new Error('Failed to parse data', { cause: err })
    }
  }

  function processFile(filename: string) {
    try {
      // 模拟文件读取错误
      throw new Error(`File not found: ${filename}`)
    } catch (err) {
      throw new Error('Failed to process file', { cause: err })
    }
  }

  try {
    processFile('config.json')
  } catch (error) {
    console.log(error.message) // 'Failed to process file'
    console.log(error.cause?.message) // 'File not found: config.json'
    
    // 错误链追踪
    let currentError = error
    while (currentError) {
      console.log('Error:', currentError.message)
      currentError = currentError.cause
    }
  }
}

RegExp Match Indices

正则表达式新增 d 标志,返回匹配的索引信息。

typescript
{
  const regex = /t(e)(st(\d?))/d
  const str = 'test1test2'
  
  const match = regex.exec(str)
  
  console.log(match[0]) // 'test1'
  console.log(match.indices[0]) // [0, 5] - 整个匹配的位置
  console.log(match.indices[1]) // [1, 2] - 第一个捕获组 'e' 的位置
  console.log(match.indices[2]) // [2, 5] - 第二个捕获组 'st1' 的位置
  console.log(match.indices[3]) // [4, 5] - 第三个捕获组 '1' 的位置

  // 命名捕获组的索引
  const namedRegex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/d
  const dateStr = '2022-12-25'
  const dateMatch = namedRegex.exec(dateStr)
  
  console.log(dateMatch.indices.groups.year) // [0, 4]
  console.log(dateMatch.indices.groups.month) // [5, 7]
  console.log(dateMatch.indices.groups.day) // [8, 10]
}

Top-level await

在模块的顶层直接使用 await,无需包装在 async 函数中。

typescript
{
  // 在模块顶层使用 await
  const fetchData = () => {
    return new Promise((resolve) => {
      setTimeout(() => resolve('Data loaded'), 1000)
    })
  }

  // 顶层 await
  const data = await fetchData()
  console.log(data) // 'Data loaded'

  // 动态导入
  const { default: moment } = await import('moment')
  
  // 条件性导入
  const locale = 'zh-cn'
  const localeModule = await import(`./locales/${locale}.js`)

  // 异步配置初始化
  const config = await fetch('/api/config').then(res => res.json())
  console.log('Config loaded:', config)
}

// 注意:需要在模块环境中使用,文件需要有 import/export 或添加 export {}
export {}