ES6+ 新特性详解:从 ES2015 到 ES2024

2024-09-01 前端

JavaScript 自 1995 年诞生以来,经历了漫长的演化。2015 年 ECMAScript 6(又称 ES2015)的发布,标志着 JavaScript 语言进入现代阶段。此后 TC39 委员会采用年度发布节奏,每年为语言带来渐进式增强。本文将系统梳理从 ES2015 到 ES2024 的所有重要特性,每个知识点都配有完整可运行的代码示例。


一、ES2015(ES6)核心特性

ES2015 是 JavaScript 历史上规模最大的一次更新,引入了大量新语法和 API,从根本上改变了 JavaScript 的编程范式。

1.1 let / const —— 块级作用域与暂时性死区

在 ES6 之前,JavaScript 只有函数作用域和全局作用域,var 声明的变量存在变量提升(hoisting)和函数级作用域问题。let 和 const 引入了块级作用域。

// 块级作用域示例
{
  var a = 1;  // 函数/全局作用域
  let b = 2;  // 块级作用域
  const c = 3; // 块级作用域,不可重新赋值
}
console.log(a); // 1
// console.log(b); // ReferenceError: b is not defined
// console.log(c); // ReferenceError: c is not defined

// 暂时性死区(Temporal Dead Zone, TDZ)
console.log(typeof x); // undefined(var 提升)
// console.log(typeof y); // ReferenceError(TDZ)
let y = 10;

// const 必须初始化,且不能重新赋值
const PI = 3.14159;
// PI = 3; // TypeError: Assignment to constant variable

// 但 const 对象属性可变
const obj = { name: 'Alice' };
obj.name = 'Bob'; // 允许
// obj = {}; // 错误

典型应用场景:let 用于循环计数器、临时变量;const 用于常量、函数表达式、模块导入、原则上所有不应重新赋值的变量。

// let 在循环中产生正确闭包
for (let i = 0; i < 5; i++) {
  setTimeout(() => console.log(i), 0); // 0, 1, 2, 3, 4
}

// var 在循环中产生错误闭包
for (var j = 0; j < 5; j++) {
  setTimeout(() => console.log(j), 0); // 5, 5, 5, 5, 5
}

1.2 箭头函数

箭头函数提供了更简洁的函数语法,并解决了传统函数中 this 绑定的痛点。

语法形式:

// 无参数
const f1 = () => 'hello';

// 单个参数(可省略括号)
const f2 = x => x * 2;

// 多个参数
const f3 = (a, b) => a + b;

// 多行函数体需用花括号 + return
const f4 = (a, b) => {
  const sum = a + b;
  return sum * 2;
};

// 返回对象字面量需加括号
const f5 = () => ({ id: 1, name: 'test' });

this 绑定机制:箭头函数没有自己的 this,它会捕获定义时所在上下文的 this 值,且一旦绑定不可改变(call、apply、bind 无效)。

function Timer() {
  this.seconds = 0;

  // 传统函数:this 指向调用者(setTimeout 的全局对象)
  setInterval(function() {
    this.seconds++; // 错误:this 指向 window/global
  }, 1000);

  // 箭头函数:this 继承自 Timer 实例
  setInterval(() => {
    this.seconds++; // 正确
  }, 1000);
}

适用场景:

  • 回调函数(setTimeout、setInterval、事件监听)
  • 数组方法(map、filter、reduce)
  • React 类组件的自定义方法

不适用场景:

  • 作为对象方法(this 不会指向对象)
  • 作为构造函数(不能使用 new)
  • 需要动态 this 的场景(如事件处理中需要 event.currentTarget)
  • 需要使用 arguments 对象时(箭头函数无 arguments)
// 不适用于对象方法
const obj = {
  name: 'Alice',
  greet: () => `Hello, ${this.name}`, // this 指向外部,不是 obj
  greetOk() {
    return `Hello, ${this.name}`; // 正确
  }
};

// 没有 arguments
const sum = () => {
  // console.log(arguments); // ReferenceError
};

// 不能作为构造函数
// const p = new (() => {}); // TypeError

1.3 模板字符串

模板字符串使用反引号 ` 包裹,支持插值表达式、多行字符串和标签模板。

const name = 'World';
const greeting = `Hello, ${name}!`; // Hello, World!

// 多行字符串
const multiLine = `
  <div>
    <p>Hello</p>
  </div>
`;

// 表达式插值
const a = 5, b = 10;
console.log(`${a} + ${b} = ${a + b}`); // 5 + 10 = 15

// 三元表达式
const score = 85;
const grade = `成绩: ${score >= 60 ? '及格' : '不及格'}`;

// 函数调用
const formatPrice = p => `¥${p.toFixed(2)}`;
console.log(formatPrice(19.9)); // ¥19.90

标签模板:允许用函数处理模板字符串,在 DSL、国际化、安全过滤等场景非常强大。

function highlight(strings, ...values) {
  return strings.reduce((result, str, i) => {
    const value = values[i] ? `<strong>${values[i]}</strong>` : '';
    return result + str + value;
  }, '');
}

const user = 'Alice';
const action = 'login';
const result = highlight`用户 ${user} 执行了 ${action} 操作`;
console.log(result); // 用户 <strong>Alice</strong> 执行了 <strong>login</strong> 操作

1.4 解构赋值

解构赋值可以从数组或对象中提取值并赋给变量,极大简化数据访问代码。

数组解构:

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

// 基本解构
const [first, second] = arr;
console.log(first, second); // 1 2

// 跳过元素
const [a, , c] = arr;
console.log(a, c); // 1 3

// 剩余模式
const [head, ...tail] = arr;
console.log(head, tail); // 1 [2, 3, 4, 5]

// 默认值
const [x = 0, y = 0, z = 0] = [1, 2];
console.log(x, y, z); // 1 2 0

// 交换变量
let p = 1, q = 2;
[p, q] = [q, p];
console.log(p, q); // 2 1

对象解构:

1.5 展开运算符与剩余参数

展开运算符(spread operator)和剩余参数(rest parameters)都使用 ... 语法,但用途不同。

展开运算符:将可迭代对象展开为多个元素。

// 数组展开
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const merged = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]

// 复制数组(浅拷贝)
const copy = [...arr1];

// 字符串展开
const chars = [...'hello']; // ['h', 'e', 'l', 'l', 'o']

// 对象展开(ES2018)
const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, ...obj1 }; // { c: 3, a: 1, b: 2 }

// 合并对象(后面的覆盖前面的)
const mergedObj = { ...obj1, ...obj2 };

// 函数调用展开
const nums = [1, 2, 3, 4, 5];
console.log(Math.max(...nums)); // 5

剩余参数:将多个参数收集为一个数组。

function sum(...numbers) {
  return numbers.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2, 3, 4)); // 10

// 剩余参数必须放在最后
function format(template, ...args) {
  return template.replace(/%s/g, () => args.shift());
}
console.log(format('Hello %s, you are %s', 'Alice', 30));
// Hello Alice, you are 30

// 与解构结合
function processConfig({ debug, ...options }) {
  console.log(debug, options);
}
processConfig({ debug: true, port: 3000, host: 'localhost' });
// true { port: 3000, host: 'localhost' }

1.6 默认参数

ES6 允许在函数参数中直接指定默认值,且默认值可以是表达式,也可以引用前面的参数。

1.7 Object.assign 与对象字面量增强

Object.assign:用于对象的合并(浅拷贝)。

const target = { a: 1 };
const source1 = { b: 2 };
const source2 = { c: 3, a: 4 };

Object.assign(target, source1, source2);
console.log(target); // { a: 4, b: 2, c: 3 }

// 常见用途:克隆对象(浅拷贝)
const clone = Object.assign({}, target);

// 合并默认配置
const DEFAULTS = { method: 'GET', timeout: 3000 };
const config = Object.assign({}, DEFAULTS, { method: 'POST' });

对象字面量增强:

const name = 'Alice';
const age = 30;

// 属性简写
const person = { name, age }; // 等同于 { name: name, age: age }

// 方法简写
const calculator = {
  add(a, b) { return a + b; },  // 等同于 add: function(a, b) { ... }
  subtract(a, b) { return a - b; }
};

// 计算属性名
const key = 'dynamicKey';
const obj = {
  [key]: 'value',
  [`${key}2`]: 'value2'
};
console.log(obj); // { dynamicKey: 'value', dynamicKey2: 'value2' }

二、Symbol 类型

Symbol 是 ES6 新增的第七种原始数据类型,表示唯一且不可变的值,常用作对象属性的唯一键。

2.1 基本用法

const s1 = Symbol();
const s2 = Symbol();
console.log(s1 === s2); // false

const s3 = Symbol('description');
const s4 = Symbol('description');
console.log(s3 === s4); // false(描述相同,值不同)
console.log(s3.toString()); // Symbol(description)
console.log(s3.description); // 'description'(ES2019)

// 作为对象属性键
const secret = Symbol('secret');
const user = {
  name: 'Alice',
  [secret]: 'hidden_value'
};
console.log(user[secret]); // hidden_value
console.log(Object.keys(user)); // ['name'] — Symbol 键不会被枚举
console.log(JSON.stringify(user)); // {"name":"Alice"}
console.log(Object.getOwnPropertySymbols(user)); // [Symbol(secret)]

2.2 Symbol.for() 与 Symbol.keyFor()

Symbol.for() 会在全局 Symbol 注册表中查找/创建 Symbol,相同键名返回同一个 Symbol。

const s1 = Symbol.for('app.key');
const s2 = Symbol.for('app.key');
console.log(s1 === s2); // true

const s3 = Symbol('app.key');
console.log(s1 === s3); // false

// Symbol.keyFor() 返回全局注册表中的键名
console.log(Symbol.keyFor(s1)); // 'app.key'
console.log(Symbol.keyFor(s3)); // undefined(非全局注册表)

2.3 内置 Symbol

JavaScript 内置了一系列 well-known Symbol,用于自定义语言内部行为。


三、Set、Map、WeakSet、WeakMap

ES6 引入了四种新的集合类型,弥补了传统 Object 和 Array 在数据管理上的不足。

3.1 Set

Set 是值不能重复的有序列表。

3.2 Map

Map 是键值对集合,键可以是任意类型(包括对象、函数、NaN),而 Object 的键只能是字符串或 Symbol。

3.3 WeakSet 与 WeakMap

WeakSet 和 WeakMap 的键是弱引用,不会阻止垃圾回收,适用于需要关联额外数据但不影响对象生命周期的场景。

WeakMap:

// WeakMap 的键必须是对象
const wm = new WeakMap();

let obj = { id: 1 };
wm.set(obj, 'some metadata');
console.log(wm.get(obj)); // 'some metadata'

obj = null; // 对象被回收,WeakMap 自动移除该条目

// 没有 size 属性,没有 forEach,不可遍历
// console.log(wm.size); // undefined

实际应用:存储私有属性:

const privateData = new WeakMap();

class User {
  constructor(name, password) {
    privateData.set(this, { password });
    this.name = name;
  }

  checkPassword(pwd) {
    return privateData.get(this).password === pwd;
  }
}

const user = new User('Alice', 'secret123');
console.log(user.checkPassword('secret123')); // true
console.log(user.password); // undefined(私密)

实际应用:DOM 关联数据:

// 假设在浏览器环境中
const elementData = new WeakMap();

function attachData(element, data) {
  elementData.set(element, data);
}

function getData(element) {
  return elementData.get(element);
}

// 当 DOM 元素被移除时,关联数据自动被垃圾回收

WeakSet:类似 WeakMap,但只存储对象值,常用于标记对象。

const visited = new WeakSet();

class Tracker {
  track(obj) {
    visited.add(obj);
  }
  hasVisited(obj) {
    return visited.has(obj);
  }
}

3.4 性能对比

特性 Object Map Array Set
键类型 String/Symbol 任意 数字索引 任意值
插入顺序 普通属性有序,整数属性特殊 有序 有序 有序
大小获取 手动计算 .size .length .size
查找性能 O(1) O(1) O(n) O(1)
频繁增删 较差 优化 一般 优化
迭代 需要 Object.keys() 直接可迭代 直接可迭代 直接可迭代

四、迭代器与生成器

迭代器(Iterator)和生成器(Generator)为 JavaScript 提供了统一的迭代协议和惰性求值能力。

4.1 Iterator 协议

一个对象要实现 Iterator 协议,必须有一个 next() 方法,返回 { value: any, done: boolean } 格式的对象。

// 手动实现一个迭代器
function createIterator(array) {
  let index = 0;
  return {
    next() {
      if (index < array.length) {
        return { value: array[index++], done: false };
      } else {
        return { value: undefined, done: true };
      }
    }
  };
}

const iterator = createIterator(['a', 'b', 'c']);
console.log(iterator.next()); // { value: 'a', done: false }
console.log(iterator.next()); // { value: 'b', done: false }
console.log(iterator.next()); // { value: 'c', done: false }
console.log(iterator.next()); // { value: undefined, done: true }

4.2 可迭代协议(Iterable Protocol)

对象实现了 [Symbol.iterator] 方法且返回一个迭代器,就是可迭代对象。可以用 for...of、展开运算符等消费。

4.3 Generator 函数

Generator 函数使用 function* 定义,返回一个 Generator 对象,既是迭代器也是可迭代对象。通过 yield 暂停函数执行,通过 next() 恢复执行。

// 基本用法
function* numberGenerator() {
  yield 1;
  yield 2;
  yield 3;
}

const gen = numberGenerator();
console.log(gen.next()); // { value: 1, done: false }
console.log(gen.next()); // { value: 2, done: false }
console.log(gen.next()); // { value: 3, done: false }
console.log(gen.next()); // { value: undefined, done: true }

// 可用 for...of 消费
for (const n of numberGenerator()) {
  console.log(n); // 1, 2, 3
}

// yield 表达式可以接收 next() 传入的值
function* interactiveGen() {
  const a = yield '请输入第一个数';
  const b = yield '请输入第二个数';
  yield `结果是: ${a + b}`;
}

const gi = interactiveGen();
console.log(gi.next());       // { value: '请输入第一个数', done: false }
console.log(gi.next(10));     // { value: '请输入第二个数', done: false }
console.log(gi.next(20));     // { value: '结果是: 30', done: false }
console.log(gi.next());       // { value: undefined, done: true }

4.4 生成器实现无限序列

生成器的惰性求值特性使其非常适合表示无限序列。

4.5 yield* 委托

yield* 可以将迭代控制委托给另一个可迭代对象或生成器。

function* gen1() {
  yield 1;
  yield 2;
}

function* gen2() {
  yield 0;
  yield* gen1(); // 委托给 gen1
  yield* [3, 4]; // 委托给数组
  yield 5;
}

console.log([...gen2()]); // [0, 1, 2, 3, 4, 5]

// 递归生成器
function* flatten(arr) {
  for (const item of arr) {
    if (Array.isArray(item)) {
      yield* flatten(item); // 递归
    } else {
      yield item;
    }
  }
}

const nested = [1, [2, [3, 4], 5], 6];
console.log([...flatten(nested)]); // [1, 2, 3, 4, 5, 6]

五、Proxy 与 Reflect

Proxy 和 Reflect 是 ES6 引入的元编程(metaprogramming)能力,允许拦截和自定义对象的基本操作。

5.1 Proxy 基础

Proxy 用于创建一个对象的代理,拦截对该对象的各种操作。

5.2 Proxy 实际应用

验证器:

function createValidator(target, schema) {
  return new Proxy(target, {
    set(obj, prop, value) {
      const rule = schema[prop];
      if (rule) {
        if (rule.type && typeof value !== rule.type) {
          throw new TypeError(`${prop} must be ${rule.type}`);
        }
        if (rule.min !== undefined && value < rule.min) {
          throw new RangeError(`${prop} must be >= ${rule.min}`);
        }
        if (rule.max !== undefined && value > rule.max) {
          throw new RangeError(`${prop} must be <= ${rule.max}`);
        }
      }
      return Reflect.set(obj, prop, value);
    }
  });
}

const userSchema = { age: { type: 'number', min: 0, max: 150 } };
const user = createValidator({}, userSchema);
user.age = 25; // 正常
// user.age = -1; // RangeError
// user.age = 'old'; // TypeError

日志/追踪:

function createLogger(target) {
  return new Proxy(target, {
    get(obj, prop) {
      const value = Reflect.get(obj, prop);
      if (typeof value === 'function') {
        return function(...args) {
          console.log(`[LOG] ${String(prop)} called with:`, args);
          const result = value.apply(this, args);
          console.log(`[LOG] ${String(prop)} returned:`, result);
          return result;
        };
      }
      return value;
    }
  });
}

const math = createLogger(Math);
// math.max(1, 2, 3); // 会有日志输出

私有属性:

function createPrivateObject(obj, privateKeys = []) {
  return new Proxy(obj, {
    get(target, prop) {
      if (privateKeys.includes(prop)) {
        return undefined;
      }
      return Reflect.get(target, prop);
    },
    set(target, prop, value) {
      if (privateKeys.includes(prop)) {
        throw new Error(`Cannot set private property: ${String(prop)}`);
      }
      return Reflect.set(target, prop, value);
    },
    has(target, prop) {
      if (privateKeys.includes(prop)) return false;
      return Reflect.has(target, prop);
    },
    ownKeys(target) {
      return Reflect.ownKeys(target).filter(k => !privateKeys.includes(k));
    }
  });
}

Vue 3 响应式原理简介:

5.3 Reflect 静态方法

Reflect 提供了与 Proxy 拦截器一一对应的静态方法,是函数式替代命令式操作的最佳实践。


六、ES2016 - ES2024 年度新特性

6.1 ES2016

Array.prototype.includes:判断数组是否包含某个元素,比 indexOf 语义更清晰,能正确处理 NaN。

const arr = [1, 2, NaN, 4];

console.log(arr.includes(2));    // true
console.log(arr.includes(5));    // false
console.log(arr.includes(NaN));  // true(indexOf 不能)

// indexOf 的局限
console.log(arr.indexOf(NaN));   // -1(错误)

指数运算符 `**:替代Math.pow`。

console.log(2 ** 3);      // 8
console.log(2 ** 10);     // 1024
console.log(2 ** -1);     // 0.5
console.log(2 ** (1/2));  // 1.414...(平方根)

// 注意优先级
console.log(2 ** 3 ** 2); // 512(右结合,相当于 2 ** (3 ** 2))

6.2 ES2017

async/await:基于 Promise 的异步语法糖,让异步代码看起来像同步代码。

Object.values / Object.entries:

const obj = { a: 1, b: 2, c: 3 };

// ES5
console.log(Object.keys(obj)); // ['a', 'b', 'c']

// ES2017
console.log(Object.values(obj));   // [1, 2, 3]
console.log(Object.entries(obj));  // [['a', 1], ['b', 2], ['c', 3]]

// 实用组合
const entries = Object.entries(obj)
  .filter(([_, v]) => v > 1)
  .map(([k, v]) => ({ [k]: v * 2 }));
console.log(entries); // [{ b: 4 }, { c: 6 }]

字符串填充:padStart / padEnd。

// padStart
console.log('5'.padStart(3, '0'));       // '005'
console.log('12345'.padStart(3, '0'));   // '12345'(长度不足不截断)
console.log('abc'.padStart(8, '123'));   // '12312abc'

// padEnd
console.log('5'.padEnd(3, '0'));         // '500'
console.log('hello'.padEnd(8, '!'));     // 'hello!!!'

// 实用:格式化时间
const hour = String(3).padStart(2, '0');
const min = String(5).padStart(2, '0');
console.log(`${hour}:${min}`); // '03:05'

6.3 ES2018

异步迭代器(Async Iterator):Symbol.asyncIterator 与 for await...of。

Promise.finally:无论 Promise 成功还是失败,最终都会执行的回调。

function showLoading(show) { /* ... */ }

function fetchUsers() {
  showLoading(true);
  return fetch('/api/users')
    .then(res => res.json())
    .catch(err => console.error(err))
    .finally(() => showLoading(false)); // 无论成功失败,关闭加载状态
}

// 适合:loading 状态、释放资源、关闭连接

对象展开(Rest/Spread Properties):

// 对象展开(前面已介绍)
const obj1 = { a: 1, b: 2 };
const obj2 = { ...obj1, c: 3 }; // { a: 1, b: 2, c: 3 }

// 对象剩余模式
const { a, ...rest } = obj2;
console.log(a);    // 1
console.log(rest); // { b: 2, c: 3 }

// 合并与覆写
const defaults = { method: 'GET', headers: {}, timeout: 5000 };
const override = { method: 'POST', headers: { 'Content-Type': 'application/json' } };
const config = { ...defaults, ...override };
// { method: 'POST', headers: { 'Content-Type': 'application/json' }, timeout: 5000 }

正则表达式 s 标志(dotAll):让 . 匹配包括换行符在内的所有字符。

console.log(/hello.world/.test('hello\nworld')); // false
console.log(/hello.world/s.test('hello\nworld')); // true

// 命名捕获组
const regex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = regex.exec('2024-09-01');
console.log(match.groups);
// { year: '2024', month: '09', day: '01' }

6.4 ES2019

Array.flat / flatMap:

const nested = [1, [2, [3, [4]]]];

// 默认展平一层
console.log(nested.flat()); // [1, 2, [3, [4]]]

// 指定展平层数
console.log(nested.flat(2));  // [1, 2, 3, [4]]
console.log(nested.flat(Infinity)); // [1, 2, 3, 4]

// flatMap = map + flat(1)
const arr = ['hello world', 'foo bar'];
console.log(arr.flatMap(s => s.split(' ')));
// ['hello', 'world', 'foo', 'bar']

// 比 map + flat 更高效

Object.fromEntries:Object.entries 的逆操作。

const entries = [['a', 1], ['b', 2], ['c', 3]];
const obj = Object.fromEntries(entries);
console.log(obj); // { a: 1, b: 2, c: 3 }

// 从 Map 转对象
const map = new Map([['x', 10], ['y', 20]]);
const fromMap = Object.fromEntries(map);
console.log(fromMap); // { x: 10, y: 20 }

// 实用:过滤对象
const data = { a: 1, b: 2, c: 3, d: 4 };
const filtered = Object.fromEntries(
  Object.entries(data).filter(([_, v]) => v > 2)
);
console.log(filtered); // { c: 3, d: 4 }

catch 可选参数:

// ES2019 之前
try {
  JSON.parse('invalid json');
} catch (err) {
  console.error('解析失败');
}

// ES2019+ 可以省略括号和参数
try {
  JSON.parse('invalid json');
} catch {
  console.error('解析失败'); // 不关心具体错误对象
}

// 适合:错误信息已由上层处理、不需要错误详情

String.trimStart / trimEnd:

const str = '   hello world   ';
console.log(str.trimStart()); // 'hello world   '
console.log(str.trimEnd());   // '   hello world'
console.log(str.trim());      // 'hello world'

// 别名:trimLeft / trimRight
console.log(str.trimLeft());  // 'hello world   '

6.5 ES2020

可选链操作符 ?.:安全访问深层嵌套属性,短路求值。

空值合并运算符 ??:当左侧为 null 或 undefined 时返回右侧值。

// || 的缺陷:会处理所有 falsy 值(0, '', false)
const count = 0;
console.log(count || 10);  // 10(非预期)
console.log(count ?? 10);  // 0(正确)

const name = '';
console.log(name || 'default'); // 'default'
console.log(name ?? 'default'); // ''

// ?? 只处理 null 和 undefined
const a = null;
const b = undefined;
const c = 0;
const d = false;

console.log(a ?? 'fallback'); // 'fallback'
console.log(b ?? 'fallback'); // 'fallback'
console.log(c ?? 'fallback'); // 0
console.log(d ?? 'fallback'); // false

// 与可选链结合
const config = {};
const port = config.server?.port ?? 3000;
console.log(port); // 3000

// 不能与 && 或 || 直接混合(需加括号)
// console.log(null || undefined ?? 'fallback'); // SyntaxError
console.log((null || undefined) ?? 'fallback'); // 'fallback'

Promise.allSettled:等待所有 Promise 完成(无论成功或失败)。

const promises = [
  Promise.resolve(1),
  Promise.reject('error'),
  Promise.resolve(3)
];

Promise.allSettled(promises).then(results => {
  console.log(results);
  // [
  //   { status: 'fulfilled', value: 1 },
  //   { status: 'rejected', reason: 'error' },
  //   { status: 'fulfilled', value: 3 }
  // ]
});

// 对比 Promise.all:任意一个失败就整体失败
// Promise.all 适合「全要或全不要」
// Promise.allSettled 适合「各算各的」

String.matchAll:返回所有匹配的迭代器。

const str = '2024-01-15, 2024-03-20, 2024-07-01';
const regex = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/g;

// ES2020
const matches = str.matchAll(regex);
for (const match of matches) {
  console.log(match[0], match.groups);
  // '2024-01-15' { year: '2024', month: '01', day: '15' }
  // '2024-03-20' { year: '2024', month: '03', day: '20' }
  // '2024-07-01' { year: '2024', month: '07', day: '01' }
}

// 转为数组
const allMatches = [...str.matchAll(regex)];
console.log(allMatches.length); // 3

globalThis:统一访问全局对象的方式。

// 浏览器中:window
// Node.js 中:global
// Web Worker 中:self

// ES2020 之前各环境不同
// 兼容写法:
const glob = typeof window !== 'undefined' ? window :
            typeof global !== 'undefined' ? global : self;

// ES2020
console.log(globalThis);

// 直接使用
globalThis.setTimeout(() => console.log('hello'), 100);

6.6 ES2021

String.replaceAll:替换字符串中所有匹配项。

const str = '2024-09-01';

// replace 只替换第一个(除非用全局正则)
console.log(str.replace('-', '/'));    // '2024/09-01'

// ES2021
console.log(str.replaceAll('-', '/')); // '2024/09/01'

// replaceAll 支持字符串或正则(必须带 g 标志)
console.log(str.replaceAll(/-/g, '.')); // '2024.09.01'

// 错误:不带 g 标志的正则
// str.replaceAll(/-/, '.'); // TypeError

Promise.any:任意一个 Promise 成功即返回,全部失败则抛出 AggregateError。

const promises = [
  Promise.reject('Error 1'),
  new Promise(resolve => setTimeout(() => resolve('慢速成功'), 500)),
  Promise.reject('Error 2')
];

Promise.any(promises)
  .then(result => console.log(result)) // '慢速成功'(500ms 后)
  .catch(err => console.log(err));

// 全部失败
Promise.any([
  Promise.reject('A'),
  Promise.reject('B')
]).catch(err => {
  console.log(err instanceof AggregateError); // true
  console.log(err.errors); // ['A', 'B']
});

// Promise 组合对比
// Promise.all      — 全部成功 || 快速失败
// Promise.allSettled — 全部完成(无论成败)
// Promise.race     — 第一个完成(无论成败)
// Promise.any      — 第一个成功 || 全部失败

数字分隔符 _:提高大数字可读性。

const billion = 1_000_000_000;
console.log(billion); // 1000000000

const price = 99_99;           // 9999
const hex = 0xFF_FF_FF;        // 16777215
const binary = 0b1010_1010;    // 170
const sci = 1_000_000_000_000; // 万亿

// 注意:不能放在开头、结尾或小数点两侧
// const invalid = _100; // SyntaxError
// const invalid2 = 100_; // SyntaxError

逻辑赋值运算符:&&=、||=、??=。

let a = 0;
let b = 1;
let c = null;

// 逻辑与赋值(a 为 truthy 时才赋值)
a &&= 10;  // a 是 0(falsy),不赋值 → a 仍然是 0
b &&= 20;  // b 是 1(truthy),赋值 → b 变为 20

// 逻辑或赋值(a 为 falsy 时才赋值)
a ||= 100; // a 是 0(falsy),赋值 → a 变为 100

// 空值合并赋值(a 为 null/undefined 时才赋值)
c ??= 200; // c 是 null,赋值 → c 变为 200

// 等价于
// a || (a = 100);
// b && (b = 20);
// c ?? (c = 200);

// 典型应用:设置默认值
let options = { timeout: 3000 };
options.timeout ??= 5000; // 已存在,不覆盖

let config = {};
config.port ??= 8080;      // 不存在,赋默认值

6.7 ES2022

Array.at:支持正负索引访问数组元素。

const arr = [10, 20, 30, 40, 50];

// 正索引
console.log(arr.at(0));  // 10
console.log(arr.at(2));  // 30

// 负索引(从末尾开始)
console.log(arr.at(-1)); // 50
console.log(arr.at(-2)); // 40

// 对比传统方式
console.log(arr[arr.length - 1]); // 50(不够优雅)
console.log(arr.slice(-1)[0]);    // 50(性能较差)

// 同样适用于 TypedArray 和 String
console.log('hello'.at(-1)); // 'o'

顶级 await (Top-level await):在模块顶层直接使用 await。

// fetch-config.js
// ES2022 之前:必须包在 async 函数中
// async function loadConfig() {
//   const res = await fetch('/config.json');
//   return res.json();
// }

// ES2022:模块顶层直接 await
const response = await fetch('/config.json');
const config = await response.json();

export default config;

// 依赖配置的模块
// import config from './fetch-config.js';
// console.log(config.apiUrl);

// 注意:顶级 await 只在 ES 模块中有效(type="module" 或 .mjs)
// 会阻塞模块加载,但有条件地阻塞(依赖该模块的模块才会等待)

Object.hasOwn:安全地检查对象自有属性。

const obj = Object.create(null); // 无原型的对象
obj.name = 'Alice';

// hasOwnProperty 可能被覆盖或不存在
// console.log(obj.hasOwnProperty('name')); // TypeError(无原型)

// ES2022
console.log(Object.hasOwn(obj, 'name')); // true
console.log(Object.hasOwn(obj, 'toString')); // false

// 对比传统方式
const regularObj = { a: 1 };
console.log(Object.hasOwn(regularObj, 'a')); // true
console.log(Object.prototype.hasOwnProperty.call(regularObj, 'a')); // true

error.cause:为错误传递原因信息。

// 创建带有原因的错误
try {
  try {
    throw new Error('数据库连接失败');
  } catch (dbError) {
    throw new Error('获取用户数据失败', { cause: dbError });
  }
} catch (err) {
  console.log(err.message);   // '获取用户数据失败'
  console.log(err.cause);     // Error: 数据库连接失败
  console.log(err.cause.message); // '数据库连接失败'
}

// 实际应用:多层错误传递
async function fetchUserData(userId) {
  try {
    const response = await fetch(`/api/users/${userId}`);
    if (!response.ok) {
      throw new Error(`HTTP ${response.status}`, { cause: response });
    }
    return await response.json();
  } catch (err) {
    throw new Error(`获取用户 ${userId} 数据失败`, { cause: err });
  }
}

Class 字段增强:公共字段、私有字段、静态字段。

静态代码块(Static initialization blocks):

class Database {
  static connection;
  static config;

  // 静态代码块 — 在类初始化时执行
  static {
    try {
      // 复杂的静态初始化逻辑
      this.config = {
        host: process.env.DB_HOST || 'localhost',
        port: parseInt(process.env.DB_PORT) || 5432
      };
      this.connection = { connected: true, ...this.config };
    } catch (err) {
      console.error('数据库初始化失败', err);
      this.connection = null;
    }
  }

  static getConnection() {
    return this.connection;
  }
}

6.8 ES2023

Array.findLast / findLastIndex:从数组末尾开始查找。

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

// 查找最后一个大于 2 的元素
const lastLarge = arr.findLast(x => x > 2);
console.log(lastLarge); // 3(最后一个大于2的)

// 查找最后一个满足条件的索引
const lastLargeIndex = arr.findLastIndex(x => x > 2);
console.log(lastLargeIndex); // 5(最后一个大于2的索引)

// 对比 find(从头找)
const firstLarge = arr.find(x => x > 2);
console.log(firstLarge); // 3(第一个大于2的)

// 实际应用:获取数组中最后一个偶数
const lastEven = arr.findLast(x => x % 2 === 0);
console.log(lastEven); // 2

Hashbang 语法:JavaScript 引擎可识别脚本第一行的 #! 注释。

#!/usr/bin/env node
// 上面的 hashbang 会被 JavaScript 引擎自动忽略
// 使得 .js 文件可以直接作为 shell 脚本执行

console.log('Hello from executable JS file!');

// 之前需要手动处理
// #!/usr/bin/env node
// 在某些环境中需要依赖工具忽略 hashbang

6.9 ES2024

Array GroupBy / GroupByToMap:按条件分组数组。

const items = [
  { name: 'apple', category: 'fruit' },
  { name: 'carrot', category: 'vegetable' },
  { name: 'banana', category: 'fruit' },
  { name: 'broccoli', category: 'vegetable' },
];

// Object.groupBy — 返回普通对象
const grouped = Object.groupBy(items, item => item.category);
console.log(grouped);
// {
//   fruit: [{ name: 'apple', category: 'fruit' }, { name: 'banana', category: 'fruit' }],
//   vegetable: [{ name: 'carrot', category: 'vegetable' }, { name: 'broccoli', category: 'vegetable' }]
// }

// Map.groupBy — 返回 Map(适合非字符串键)
const numbers = [1, 2, 3, 4, 5, 6];
const parityGroups = Map.groupBy(numbers, n => n % 2 === 0 ? 'even' : 'odd');
console.log(parityGroups.get('even')); // [2, 4, 6]
console.log(parityGroups.get('odd'));  // [1, 3, 5]

// 按属性长度分组
const words = ['hello', 'world', 'hi', 'js', 'typescript'];
const byLength = Object.groupBy(words, w => w.length);
console.log(byLength);
// { '2': ['hi', 'js'], '5': ['hello', 'world'], '10': ['typescript'] }

Promise.withResolvers:在 Promise 外部 resolve/reject。

// ES2024 之前
function createDeferred() {
  let resolve, reject;
  const promise = new Promise((res, rej) => {
    resolve = res;
    reject = rej;
  });
  return { promise, resolve, reject };
}

// ES2024
const { promise, resolve, reject } = Promise.withResolvers();

// 可以在任意地方 resolve/reject
setTimeout(() => resolve('完成'), 1000);

const result = await promise;
console.log(result); // '完成'

// 实际应用:事件转 Promise
function waitForEvent(element, eventName) {
  const { promise, resolve } = Promise.withResolvers();
  element.addEventListener(eventName, resolve, { once: true });
  return promise;
}

正则表达式 v 标志:增强的 Unicode 属性转义和集合操作。

// v 标志启用 Unicode 属性增强
const regex = /\p{Emoji}/v;
console.log(regex.test('😀')); // true
console.log(regex.test('a'));  // false

// 集合操作:差集、交集、并集
// 匹配 ASCII 字母但不包括元音
const consonant = /[\p{ASCII}--[aeiou]]/v;
console.log(consonant.test('b')); // true
console.log(consonant.test('a')); // false

// 交集
const hexDigit = /[\p{ASCII}&&\p{Hex_Digit}]/v;
console.log(hexDigit.test('A')); // true
console.log(hexDigit.test('Z')); // false(Z 不是十六进制字符)

// 注意:v 标志需要现代化引擎支持

七、总结

从 ES2015 的里程碑式发布到 ES2024 的渐进增强,JavaScript 已经成长为一门功能完备、表达力极强的语言。下表概括了各版本的核心贡献:

版本 核心贡献
ES2015 模块化、箭头函数、Promise、类、let/const、解构、模板字符串、Symbol、Set/Map、Proxy、迭代器/生成器
ES2016 Array.includes、指数运算符
ES2017 async/await、Object.values/entries、字符串填充
ES2018 异步迭代器、Promise.finally、对象/正则展开
ES2019 flat/flatMap、Object.fromEntries、catch 可选
ES2020 可选链、空值合并、Promise.allSettled、globalThis
ES2021 replaceAll、Promise.any、数字分隔符、逻辑赋值
ES2022 Array.at、顶级 await、Object.hasOwn、class 字段、error.cause
ES2023 findLast/findLastIndex、Hashbang
ES2024 GroupBy、Promise.withResolvers、正则 v 标志

学习建议:

  1. 优先掌握 ES2015 核心:80% 的日常开发都围绕 ES2015 的语法展开
  2. 渐进式吸收:每年新特性通常只有 2-4 个,逐个理解并应用到项目中
  3. 关注 TC39 流程:Stage 3 及以上特性基本稳定,可提前了解
  4. 实践出真知:最好的学习方式是在实际项目中有意识地使用新特性

JavaScript 仍在快速演进,保持学习的热情,你将能用这门语言构建任何你想构建的东西。Happy coding!