Skip to content

ECMAScript 2019(ES10)

Optional catch binding

catch 子句可以省略参数,当不需要使用错误对象时更简洁。

javascript
{
  // 传统写法 - 必须提供参数
  function isValidJSONOld(text) {
    try {
      JSON.parse(text)
      return true
    } catch (error) {  // 即使不使用 error 也必须声明
      return false
    }
  }

  // ES10 新写法 - 可以省略参数
  function isValidJSON(text) {
    try {
      JSON.parse(text)
      return true
    } catch {  // 省略错误参数
      return false
    }
  }

  const validJson = '{ "key1": "value1", "key2": "value2" }'
  const invalidJson = '{ key1: value1 }'

  console.log(isValidJSON(validJson))   // true
  console.log(isValidJSON(invalidJson)) // false

  // 实际应用:功能检测
  function hasLocalStorage() {
    try {
      localStorage.setItem('test', 'test')
      localStorage.removeItem('test')
      return true
    } catch {
      return false
    }
  }

  // 检查是否支持某个 API
  function supportsWebP() {
    try {
      return document.createElement('canvas')
        .toDataURL('image/webp')
        .indexOf('data:image/webp') === 0
    } catch {
      return false
    }
  }
}

Array.prototype.flat()

按指定深度递归地将所有子数组连接,并返回一个新数组。

javascript
{
  // 基本用法
  const arr1 = [1, 2, [3, 4]]
  const arr2 = [1, 2, [3, 4, [5, 6]]]
  const arr3 = [1, 2, [3, 4, [5, 6, [7, 8]]]]

  console.log(arr1.flat())        // [1, 2, 3, 4]
  console.log(arr2.flat())        // [1, 2, 3, 4, [5, 6]] (默认深度为1)
  console.log(arr2.flat(2))       // [1, 2, 3, 4, 5, 6]
  console.log(arr3.flat(Infinity)) // [1, 2, 3, 4, 5, 6, 7, 8] (完全扁平化)

  // 处理稀疏数组
  const sparse = [1, 2, , 4, [5, , 7]]
  console.log(sparse.flat()) // [1, 2, 4, 5, 7] (移除空槽)

  // 实际应用:数据结构扁平化
  const categories = [
    ['EF', ['JavaScript', 'TypeScript', 'React']],
    ['BE', ['Node.js', 'Python', 'Java']],
    ['DB', ['MySQL', 'MongoDB']]
  ]

  const allTechnologies = categories
    .map(([category, techs]) => techs)
    .flat()
  
  console.log(allTechnologies)
  // ['JavaScript', 'TypeScript', 'React', 'Node.js', 'Python', 'Java', 'MySQL', 'MongoDB']

  // 处理嵌套的用户权限
  const userRoles = [
    'user',
    ['admin', ['read', 'write', ['delete', 'modify']]],
    'guest'
  ]

  const flatRoles = userRoles.flat(3)
  console.log(flatRoles) // ['user', 'admin', 'read', 'write', 'delete', 'modify', 'guest']
}

Array.prototype.flatMap()

首先使用映射函数映射每个元素,然后将结果压缩成一个新数组。

javascript
{
  const numbers = [1, 2, 3, 4]

  // 对比 map() 和 flatMap()
  console.log(numbers.map(x => [x * 2]))      // [[2], [4], [6], [8]]
  console.log(numbers.flatMap(x => [x * 2]))  // [2, 4, 6, 8]

  // 等价于 map().flat(),但 flatMap 更高效
  console.log(numbers.map(x => [x * 2]).flat()) // [2, 4, 6, 8]

  // 处理多层嵌套
  console.log(numbers.flatMap(x => [[x * 2]])) // [[2], [4], [6], [8]]

  // 实际应用:字符串分词
  const sentences = ['Hello world', 'How are you', 'Fine thanks']
  const words = sentences.flatMap(sentence => sentence.split(' '))
  console.log(words) // ['Hello', 'world', 'How', 'are', 'you', 'Fine', 'thanks']

  // 处理异构数据
  const data = [
    { type: 'user', items: ['Alice', 'Bob'] },
    { type: 'admin', items: ['Charlie'] },
    { type: 'guest', items: [] }
  ]

  const allUsers = data.flatMap(group => group.items)
  console.log(allUsers) // ['Alice', 'Bob', 'Charlie']

  // 条件性展开
  const mixedData = [1, 2, 3, 4, 5]
  const result = mixedData.flatMap(x => 
    x % 2 === 0 ? [x, x * 2] : [x]  // 偶数展开为两个值
  )
  console.log(result) // [1, 2, 4, 3, 4, 8, 5]

  // 过滤并转换
  const items = ['apple', '', 'banana', null, 'cherry']
  const validItems = items.flatMap(item => 
    item && item.length > 0 ? [item.toUpperCase()] : []
  )
  console.log(validItems) // ['APPLE', 'BANANA', 'CHERRY']
}

Function.prototype.toString() 修订

toString() 方法现在返回精确的源代码,包括空格和注释。

javascript
{
  // 用户定义的函数
  function myFunction() {
    // 这是注释
    console.log('Hello World')
  }

  console.log(myFunction.toString())
  // function myFunction() {
  //   // 这是注释
  //   console.log('Hello World')
  // }

  // 箭头函数
  const arrowFn = (x, y) => x + y
  console.log(arrowFn.toString()) // (x, y) => x + 1

  // 内置函数
  console.log(parseInt.toString()) // function parseInt() { [native code] }
  console.log(Math.max.toString()) // function max() { [native code] }

  // 绑定函数
  const boundFn = myFunction.bind(null)
  console.log(boundFn.toString()) // function () { [native code] }

  // 异步函数
  async function asyncFn() {
    await Promise.resolve()
  }
  console.log(asyncFn.toString())
  // async function asyncFn() {
  //   await Promise.resolve()
  // }

  // 生成器函数
  function* generatorFn() {
    yield 1
    yield 2
  }
  console.log(generatorFn.toString())
  // function* generatorFn() {
  //   yield 1
  //   yield 2
  // }

  // 实际应用:代码分析和调试
  function analyzeFunction(fn) {
    const source = fn.toString()
    return {
      isAsync: source.includes('async'),
      isGenerator: source.includes('function*'),
      isArrow: source.includes('=>'),
      lineCount: source.split('\n').length,
      hasComments: source.includes('//') || source.includes('/*')
    }
  }

  console.log(analyzeFunction(myFunction))
  // { isAsync: false, isGenerator: false, isArrow: false, lineCount: 4, hasComments: true }
}

Well-formed JSON.stringify

修复 JSON.stringify() 对于某些 Unicode 字符的处理。

javascript
{
  // ES10 之前的问题:不正确处理代理对
  console.log(JSON.stringify('\uD83D\uDE0E')) // "😎" (正确)
  
  // 孤立的代理字符现在能正确处理
  console.log(JSON.stringify('\uD800'))       // "\\ud800"
  console.log(JSON.stringify('\uDFFF'))       // "\\udfff"

  // 混合的正常字符和代理字符
  const mixedString = 'Hello \uD83D\uDE0E World \uD800'
  console.log(JSON.stringify(mixedString))    // "Hello 😎 World \\ud800"

  // 实际应用:安全的数据序列化
  function safeStringify(obj) {
    try {
      return JSON.stringify(obj)
    } catch (error) {
      console.error('JSON.stringify failed:', error)
      return null
    }
  }

  // 处理包含各种 Unicode 字符的对象
  const unicodeData = {
    emoji: '🚀✨🎉',
    chinese: '你好世界',
    arabic: 'مرحبا بالعالم',
    surrogate: '\uD83D\uDE0E',
    invalid: '\uD800'
  }

  console.log(safeStringify(unicodeData))
  // {"emoji":"🚀✨🎉","chinese":"你好世界","arabic":"مرحبا بالعالم","surrogate":"😎","invalid":"\\ud800"}
}

Object.fromEntries()

将键值对列表转换为对象,是 Object.entries() 的逆操作。

javascript
{
  // 基本用法
  const entries = [
    ['name', 'Alice'],
    ['age', 25],
    ['city', 'New York']
  ]

  const person = Object.fromEntries(entries)
  console.log(person) // { name: 'Alice', age: 25, city: 'New York' }

  // 与 Object.entries() 的对称性
  const original = { a: 1, b: 2, c: 3 }
  const reconstructed = Object.fromEntries(Object.entries(original))
  console.log(reconstructed) // { a: 1, b: 2, c: 3 }

  // 从 Map 创建对象
  const map = new Map([
    ['foo', 'bar'],
    ['baz', 42]
  ])
  const objFromMap = Object.fromEntries(map)
  console.log(objFromMap) // { foo: 'bar', baz: 42 }

  // 数组转对象
  const indexedArray = [
    ['0', 'first'],
    ['1', 'second'],
    ['2', 'third']
  ]
  const objFromArray = Object.fromEntries(indexedArray)
  console.log(objFromArray) // { '0': 'first', '1': 'second', '2': 'third' }

  // 实际应用:数据转换和过滤
  const rawData = {
    name: 'John',
    age: 30,
    password: 'secret123',
    email: 'john@example.com',
    internal_id: 12345
  }

  // 过滤敏感数据
  const publicData = Object.fromEntries(
    Object.entries(rawData).filter(([key]) => 
      !key.startsWith('password') && !key.startsWith('internal_')
    )
  )
  console.log(publicData) // { name: 'John', age: 30, email: 'john@example.com' }

  // 转换数据格式
  const scores = {
    math: 85,
    science: 92,
    english: 78,
    history: 88
  }

  const gradeMap = {
    A: score => score >= 90,
    B: score => score >= 80,
    C: score => score >= 70,
    D: score => score >= 60,
    F: score => score < 60
  }

  const grades = Object.fromEntries(
    Object.entries(scores).map(([subject, score]) => {
      const grade = Object.keys(gradeMap).find(g => gradeMap[g](score))
      return [subject, grade]
    })
  )
  console.log(grades) // { math: 'B', science: 'A', english: 'C', history: 'B' }

  // URL 查询参数解析
  function parseQuery(queryString) {
    const params = new URLSearchParams(queryString)
    return Object.fromEntries(params)
  }

  const query = 'name=Alice&age=25&city=New%20York'
  console.log(parseQuery(query)) // { name: 'Alice', age: '25', city: 'New York' }
}

Symbol.prototype.description

提供 Symbol 的描述字符串,用于调试目的。

javascript
{
  // 创建带描述的 Symbol
  const sym1 = Symbol('debug info')
  const sym2 = Symbol('user-id')
  const sym3 = Symbol() // 无描述

  console.log(sym1.description) // 'debug info'
  console.log(sym2.description) // 'user-id'
  console.log(sym3.description) // undefined

  // Symbol 的唯一性不受描述影响
  console.log(Symbol('same') === Symbol('same')) // false
  console.log(sym1 === sym1) // true

  // 实际应用:调试和日志
  class EventEmitter {
    constructor() {
      this.events = new Map()
    }

    on(eventType, callback) {
      const symbol = Symbol(`event-${eventType}`)
      this.events.set(symbol, { type: eventType, callback })
      return symbol
    }

    off(symbol) {
      if (this.events.has(symbol)) {
        console.log(`Removing event: ${symbol.description}`)
        this.events.delete(symbol)
      }
    }

    listEvents() {
      const events = []
      for (const [symbol, data] of this.events) {
        events.push({
          symbol: symbol.description,
          type: data.type
        })
      }
      return events
    }
  }

  const emitter = new EventEmitter()
  const clickHandler = emitter.on('click', () => {})
  const keyHandler = emitter.on('keypress', () => {})

  console.log(emitter.listEvents())
  // [
  //   { symbol: 'event-click', type: 'click' },
  //   { symbol: 'event-keypress', type: 'keypress' }
  // ]

  // 私有属性标识
  const PRIVATE_PROPS = {
    userId: Symbol('private-user-id'),
    apiKey: Symbol('private-api-key'),
    internalState: Symbol('private-internal-state')
  }

  class User {
    constructor(id, name) {
      this[PRIVATE_PROPS.userId] = id
      this.name = name
    }

    getDebugInfo() {
      const props = Object.getOwnPropertySymbols(this)
      return props.map(prop => ({
        description: prop.description,
        value: typeof this[prop]
      }))
    }
  }

  const user = new User(123, 'Alice')
  console.log(user.getDebugInfo())
  // [{ description: 'private-user-id', value: 'number' }]
}

String.prototype.trimStart() / trimEnd()

分别移除字符串开头和结尾的空白字符。

javascript
{
  const str = '   Hello World!   '

  // 新方法
  console.log(str.trimStart()) // 'Hello World!   '
  console.log(str.trimEnd())   // '   Hello World!'
  console.log(str.trim())      // 'Hello World!'

  // 别名(向后兼容)
  console.log(str.trimLeft())  // 'Hello World!   ' (trimStart 的别名)
  console.log(str.trimRight()) // '   Hello World!' (trimEnd 的别名)

  // 验证别名关系
  console.log(String.prototype.trimLeft.name === 'trimStart')   // true
  console.log(String.prototype.trimRight.name === 'trimEnd')    // true

  // 处理不同类型的空白字符
  const whitespaceStr = '\t\n   Hello\t\n   '
  console.log(whitespaceStr.trimStart()) // 'Hello\t\n   '
  console.log(whitespaceStr.trimEnd())   // '\t\n   Hello'

  // 实际应用:代码格式化
  function formatCode(code) {
    return code
      .split('\n')
      .map(line => line.trimEnd()) // 移除行尾空白
      .join('\n')
      .trimStart() // 移除开头空行
  }

  const messyCode = `
    
    function hello() {
      console.log('Hello');    
    }    
  `

  console.log(formatCode(messyCode))
  // function hello() {
  //   console.log('Hello');
  // }

  // 解析用户输入
  function parseUserInput(input) {
    const lines = input.split('\n')
    return lines
      .map(line => line.trimEnd()) // 移除每行末尾空格
      .filter(line => line.trimStart() !== '') // 过滤空行
      .map(line => line.trimStart()) // 移除每行开头空格
  }

  const userInput = `
    First item    
       Second item
    
    Third item   
  `

  console.log(parseUserInput(userInput))
  // ['First item', 'Second item', 'Third item']

  // 处理配置文件
  function parseConfig(configText) {
    const config = {}
    const lines = configText.split('\n')

    for (const line of lines) {
      const trimmed = line.trim()
      if (trimmed && !trimmed.startsWith('#')) {
        const [key, ...valueParts] = trimmed.split('=')
        if (key && valueParts.length > 0) {
          config[key.trimEnd()] = valueParts.join('=').trimStart()
        }
      }
    }

    return config
  }

  const configFile = `
    # Database configuration
    host = localhost    
    port = 5432
    database =   myapp   
    # user = admin
    timeout = 30
  `

  console.log(parseConfig(configFile))
  // { host: 'localhost', port: '5432', database: 'myapp', timeout: '30' }
}