ECMAScript 新特性与最佳实践
ECMAScript 新特性与最佳实践
ECMAScript 是 JavaScript 语言的标准规范,由 TC39 委员会(Technical Committee 39)负责制定和维护。自 2015 年发布 ES6(ES2015)以来,ECMAScript 改为每年发布一个新版本,以年份命名,如 ES2016、ES2017、ES2018 等。这种"小步快跑"的发布节奏让 JavaScript 语言持续演进,每年都会带来许多实用的新特性和语法糖。
这些新特性不仅提高了开发效率和代码质量,还让 JavaScript 代码更加简洁、可读、可维护。理解并掌握这些新特性对于现代 JavaScript 开发至关重要。无论是前端框架(React、Vue、Angular)还是后端环境(Node.js、Deno、Bun),都大量依赖这些现代语法。
TC39 提案流程与版本演进
为什么需要了解提案流程?
在深入学习具体特性之前,理解一个特性是如何从想法变成标准的,有助于我们判断某个特性是否可以放心在生产环境使用。TC39 的提案流程分为 5 个阶段(Stage 0 到 Stage 4):
为什么重要: 处于 Stage 3 的特性通常已被主流引擎实现,可以配合 Babel 等工具在生产环境使用;而 Stage 4 的特性则一定会出现在下一个年度版本中。了解阶段有助于评估技术风险。
版本一览:
| 版本 | 发布年份 | 代表特性 |
| --- | --- | --- |
| ES6 / ES2015 | 2015 | let/const、箭头函数、模板字符串、解构、Class、Module、Promise、Map/Set |
| ES2016 | 2016 | 指数运算符、Array.includes |
| ES2017 | 2017 | async/await、Object.values/entries、字符串填充 |
| ES2018 | 2018 | 异步迭代、对象展开、正则改进 |
| ES2019 | 2019 | flat/flatMap、Object.fromEntries、trimStart/End |
| ES2020 | 2020 | 可选链、空值合并、BigInt、动态 import、Promise.allSettled |
| ES2021 | 2021 | 逻辑赋值、数字分隔符、replaceAll、Promise.any、WeakRef |
| ES2022 | 2022 | Top-level await、私有字段、static 块、at()、Object.hasOwn |
| ES2023 | 2023 | findLast/findLastIndex、toSorted/toReversed、Array.group(后延) |
| ES2024 | 2024 | Object.groupBy/Map.groupBy、Promise.withResolvers、ArrayBuffer 改进 |
ES6 (ES2015) 核心特性
ES6 是 JavaScript 历史上最重要的一次更新,它奠定了现代 JavaScript 的基础。下面逐一深入讲解 ES6 的核心特性。
let 和 const 声明:
ES6 引入了 let 和 const 关键字,解决了 var 声明的变量提升和函数作用域问题。let 声明的变量具有块级作用域,const 声明的变量是常量,不可重新赋值。使用 let 和 const 可以避免很多常见的错误,提高代码的可预测性。
为什么重要: var 存在变量提升、无块级作用域、可重复声明等问题,是许多隐蔽 bug 的根源。let/const 通过引入块级作用域和暂时性死区(TDZ,Temporal Dead Zone),让变量的行为更符合直觉。
原理: let/const 声明的变量会被提升到块的顶部,但在声明语句执行之前处于"暂时性死区",访问会抛出 ReferenceError。而 var 声明的变量会被初始化为 undefined。
// var 的问题
console.log(a); // undefined(变量提升)
var a = 10;
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 输出: 3, 3, 3(函数作用域问题)
// let 解决变量提升(暂时性死区)
console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 10;
// let 解决循环问题
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 100);
}
// 输出: 0, 1, 2(块级作用域,每次迭代都是新的绑定)
// const 常量
const PI = 3.14159;
// PI = 3.14; // TypeError: Assignment to constant variable
// const 对象属性可修改(const 只保证绑定不变,不保证值不可变)
const user = { name: 'Alice' };
user.name = 'Bob'; // OK
// user = {}; // TypeError
// 冻结对象使其完全不可变(浅冻结)
const frozenUser = Object.freeze({ name: 'Alice' });
// frozenUser.name = 'Bob'; // 静默失败(严格模式下报错)
// 深冻结需要递归处理
function deepFreeze(obj) {
Object.keys(obj).forEach((key) => {
const value = obj[key];
if (typeof value === 'object' && value !== null) {
deepFreeze(value);
}
});
return Object.freeze(obj);
}
// 重复声明检测
// let x = 1;
// let x = 2; // SyntaxError: Identifier 'x' has already been declaredvar / let / const 对比:
| 特性 | var | let | const |
| --- | --- | --- | --- |
| 作用域 | 函数作用域 | 块级作用域 | 块级作用域 |
| 变量提升 | 提升并初始化为 undefined | 提升但存在 TDZ | 提升但存在 TDZ |
| 重复声明 | 允许 | 报错 | 报错 |
| 重新赋值 | 允许 | 允许 | 不允许 |
| 声明时必须初始化 | 否 | 否 | 是 |
| 挂载到全局对象 | 是(全局作用域下) | 否 | 否 |
最佳实践: 默认使用 const,只有确实需要重新赋值时才用 let,彻底避免使用 var。这样能最大限度地减少变量被意外修改的可能性,也让阅读代码的人一眼看出哪些变量是常量。
箭头函数:
箭头函数提供了更简洁的函数语法,并且自动绑定词法 this。箭头函数没有自己的 this、arguments、super 或 new.target,适合用于回调函数和需要保持外层 this 上下文的场景。
为什么重要: 在 ES5 时代,回调函数中的 this 指向问题需要通过 var self = this、.bind(this) 或传入第二个参数等方式解决,代码冗长且容易出错。箭头函数从语法层面解决了这个问题。
// 传统函数
const addTraditional = function(a, b) {
return a + b;
};
// 箭头函数
const add = (a, b) => a + b;
// 单参数可省略括号
const double = x => x * 2;
// 无参数需要空括号
const random = () => Math.random();
// 返回对象需要用括号包裹
const createUser = (name, age) => ({ name, age });
// 多行函数体需要显式 return
const process = (data) => {
const result = data.filter(Boolean);
return result.map((x) => x * 2);
};
// this 绑定示例
const obj = {
name: 'Alice',
// 传统函数:this 指向调用者
greetTraditional: function() {
setTimeout(function() {
console.log('Hello, ' + this.name); // undefined(this 丢失)
}, 100);
},
// 箭头函数:this 继承自外层
greetArrow: function() {
setTimeout(() => {
console.log('Hello, ' + this.name); // 'Hello, Alice'
}, 100);
},
// 箭头函数作为方法:this 指向外层作用域(不推荐)
greetMethod: () => {
console.log('Hello, ' + this.name); // undefined(this 指向全局/模块)
},
};
// 箭头函数不能作为构造函数
const Person = (name) => {
this.name = name;
};
// new Person('Alice'); // TypeError: Person is not a constructor
// 箭头函数没有 arguments,用 rest 参数代替
const fn = (...args) => {
console.log(args); // 正常工作
};常见坑: 不要在需要动态 this 的场景(如对象方法、事件处理器需要 this 指向 DOM 元素、Vue 的 methods)使用箭头函数。箭头函数的 this 在定义时就确定了,无法通过 call/apply/bind 改变。
模板字符串:
模板字符串使用反引号(`)包裹,支持多行字符串、变量插值和表达式嵌入。模板字符串让字符串拼接更加直观和方便,特别适合生成 HTML 模板、SQL 查询等场景。
原理: ${expression} 中的表达式会被求值并转换为字符串后插入。标签模板(Tagged Template)则允许用函数处理模板字符串的各个部分,实现更强大的字符串处理逻辑。
// 基本用法
const name = 'Alice';
const greeting = `Hello, ${name}!`;
console.log(greeting); // 'Hello, Alice!'
// 多行字符串
const title = '标题';
const content = '正文';
const html = `
<div class="card">
<h2>${title}</h2>
<p>${content}</p>
</div>
`;
// 表达式嵌入
const a = 10;
const b = 20;
console.log(`${a} + ${b} = ${a + b}`); // '10 + 20 = 30'
// 三元表达式嵌入
const score = 85;
console.log(`成绩:${score >= 60 ? '及格' : '不及格'}`);
// 嵌套模板
const items = ['Apple', 'Banana', 'Orange'];
const list = `
<ul>
${items.map((item) => `<li>${item}</li>`).join('')}
</ul>
`;
// 标签模板:实现高亮功能
function highlight(strings, ...values) {
return strings.reduce((result, str, i) => {
const value = values[i] ? `<mark>${values[i]}</mark>` : '';
return result + str + value;
}, '');
}
const search = 'JavaScript';
const text = highlight`Learning ${search} is fun!`;
// 'Learning <mark>JavaScript</mark> is fun!'
// 标签模板:实现安全的 HTML 转义
function safeHtml(strings, ...values) {
const escape = (str) =>
String(str)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>');
return strings.reduce(
(acc, str, i) => acc + str + (i < values.length ? escape(values[i]) : ''),
''
);
}
// 原始字符串:String.raw 不处理转义序列
const raw = String.raw`Line 1\nLine 2`;
console.log(raw); // 'Line 1\nLine 2'(\n 不被转义为换行)常见坑: 模板字符串直接拼接用户输入到 HTML 中会带来 XSS 注入风险。生成 DOM 时应使用 textContent 或专门的转义函数,生成 SQL 时应使用参数化查询而非字符串拼接。
解构赋值:
解构赋值允许从数组或对象中提取值,并赋给变量。解构赋值可以简化代码,使数据提取更加直观。支持数组解构、对象解构、函数参数解构等多种形式。
// 数组解构
const [first, second, ...rest] = [1, 2, 3, 4, 5];
console.log(first); // 1
console.log(second); // 2
console.log(rest); // [3, 4, 5]
// 跳过元素
const [, , third] = [1, 2, 3];
console.log(third); // 3
// 默认值
const [a = 10, b = 20] = [1];
console.log(a, b); // 1, 20
// 交换变量(无需临时变量)
let x = 1, y = 2;
[x, y] = [y, x];
console.log(x, y); // 2, 1
// 对象解构
const user = { name: 'Alice', age: 25, city: 'NYC' };
const { name, age, country = 'USA' } = user;
console.log(name, age, country); // 'Alice', 25, 'USA'
// 重命名
const { name: userName, age: userAge } = user;
console.log(userName, userAge); // 'Alice', 25
// 重命名并设置默认值
const { role: userRole = 'guest' } = user;
console.log(userRole); // 'guest'
// 嵌套解构
const company = {
name: 'Tech Corp',
address: {
city: 'San Francisco',
country: 'USA',
},
};
const { address: { city } } = company;
console.log(city); // 'San Francisco'
// 函数参数解构
function greet({ name, age = 0 }) {
console.log(`Hello, ${name}! You are ${age} years old.`);
}
greet({ name: 'Alice', age: 25 });
// 解构配合默认参数(避免传入 undefined 时报错)
function fetchData({ url, method = 'GET', headers = {} } = {}) {
console.log(url, method, headers);
}
fetchData({ url: '/api/data' });
fetchData(); // 不报错,全部使用默认值
// 从函数返回值解构
function getCoords() {
return { x: 10, y: 20 };
}
const { x: px, y: py } = getCoords();
// 解构配合迭代(遍历 Map)
const map = new Map([['a', 1], ['b', 2]]);
for (const [key, value] of map) {
console.log(key, value);
}默认参数:
ES6 允许为函数参数设置默认值,当参数为 undefined 时使用默认值。默认参数使函数更加健壮,减少了对参数检查的需求。注意:只有传入 undefined 时才会触发默认值,传入 null 不会。
// 基本默认参数
function greet(name = 'World') {
console.log(`Hello, ${name}!`);
}
greet(); // 'Hello, World!'
greet('Alice'); // 'Hello, Alice!'
greet(undefined); // 'Hello, World!'(undefined 触发默认值)
greet(null); // 'Hello, null!'(null 不触发默认值)
// 默认参数可以是表达式或函数调用
function computeDefault() {
return Date.now();
}
function getValue(value = computeDefault()) {
return value;
}
// 默认参数可以引用前面的参数
function greetFull(name, greeting = `Hello, ${name}`) {
console.log(greeting);
}
greetFull('Alice'); // 'Hello, Alice'
// 默认参数与解构结合
function createUser({ name = 'Anonymous', age = 0 } = {}) {
return { name, age };
}
createUser(); // { name: 'Anonymous', age: 0 }
createUser({ name: 'Alice' }); // { name: 'Alice', age: 0 }
// 用默认参数实现必需参数校验
function required(paramName) {
throw new Error(`Parameter ${paramName} is required`);
}
function registerUser(name = required('name'), age) {
return { name, age };
}
// registerUser(); // Error: Parameter name is required剩余参数和扩展运算符:
剩余参数(...args)将多余的参数收集为数组,扩展运算符(...)将数组或对象展开。这两个特性让函数参数处理和数据操作更加灵活。注意:剩余参数是真正的数组,而 arguments 只是类数组对象。
// 剩余参数
function sum(...numbers) {
return numbers.reduce((total, n) => total + n, 0);
}
sum(1, 2, 3, 4, 5); // 15
// 剩余参数必须是最后一个参数
function log(level, ...messages) {
console.log(`[${level}]`, ...messages);
}
log('INFO', 'User', 'logged in'); // '[INFO] User logged in'
// 扩展运算符 - 数组合并
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const merged = [...arr1, ...arr2]; // [1, 2, 3, 4, 5, 6]
// 复制数组(浅拷贝)
const copy = [...arr1];
// 在中间插入元素
const inserted = [...arr1, 99, ...arr2];
// 扩展运算符 - 对象合并
const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };
const mergedObj = { ...obj1, ...obj2 }; // { a: 1, b: 2, c: 3, d: 4 }
// 覆盖属性(后面的覆盖前面的)
const config = { timeout: 1000, retry: 3 };
const customConfig = { ...config, timeout: 2000 }; // { timeout: 2000, retry: 3 }
// 函数调用中使用扩展
const numbers = [1, 2, 3];
Math.max(...numbers); // 3
Math.max.apply(null, numbers); // ES5 方式
// 类数组转数组
const nodeList = document.querySelectorAll('div');
const nodes = [...nodeList];
// 字符串转数组(正确处理 Unicode)
const chars = [...'hello']; // ['h', 'e', 'l', 'l', 'o']
const emoji = [...'👨👩👧']; // 正确按码点拆分常见坑: 扩展运算符实现的是浅拷贝,嵌套对象/数组仍是引用共享。需要深拷贝时应使用 structuredClone(后文介绍)或专门的库。
Class 类
Class 是 ES6 引入的语法糖,本质上是基于原型继承的封装。它让面向对象编程的写法更加清晰、更接近传统 OOP 语言,但底层仍然是原型链机制。此后每个年度版本又为 Class 增加了大量能力:私有字段(ES2022)、static 块(ES2022)等。
为什么重要: 在 ES5 中定义类需要通过构造函数 + prototype 手动组装,继承需要借助 Object.create 等复杂技巧,代码难读易错。Class 提供了统一、清晰的语法。
// 基本类定义
class Animal {
// 实例字段(ES2022 正式标准化)
legs = 4;
constructor(name, sound) {
this.name = name;
this.sound = sound;
}
// 实例方法
speak() {
return `${this.name} says ${this.sound}`;
}
// getter
get description() {
return `${this.name} has ${this.legs} legs`;
}
// setter
set nickname(value) {
this._nickname = value.trim();
}
get nickname() {
return this._nickname;
}
// 静态方法(挂在类上而非实例上)
static create(name, sound) {
return new Animal(name, sound);
}
// 静态字段
static kingdom = 'Animalia';
}
const dog = new Animal('Dog', 'Woof');
console.log(dog.speak()); // 'Dog says Woof'
console.log(dog.description); // 'Dog has 4 legs'
console.log(Animal.kingdom); // 'Animalia'
const cat = Animal.create('Cat', 'Meow');继承:
子类通过 extends 继承父类,通过 super 调用父类构造函数和方法。
class Dog extends Animal {
constructor(name) {
super(name, 'Woof'); // 必须先调用 super
this.breed = 'unknown';
}
// 方法重写
speak() {
return `${super.speak()} loudly!`; // super 调用父类方法
}
fetch() {
return `${this.name} fetches the ball`;
}
}
const rex = new Dog('Rex');
console.log(rex.speak()); // 'Rex says Woof loudly!'
console.log(rex instanceof Dog); // true
console.log(rex instanceof Animal); // true(原型链)私有字段和私有方法(ES2022):
使用 # 前缀声明私有字段和方法,只能在类内部访问,从外部访问会抛出语法错误。这是真正的语言级封装,而非命名约定(如 _name)。
为什么重要: 在 # 出现之前,"私有"属性只能通过 _ 前缀约定或闭包模拟,前者无强制性、后者写法繁琐。# 提供了引擎层面强制的封装。
class BankAccount {
// 私有字段
#balance = 0;
#pin;
// 静态私有字段
static #instanceCount = 0;
constructor(initialBalance, pin) {
this.#balance = initialBalance;
this.#pin = pin;
BankAccount.#instanceCount++;
}
// 私有方法
#validatePin(pin) {
return this.#pin === pin;
}
deposit(amount) {
if (amount <= 0) throw new Error('金额必须为正');
this.#balance += amount;
return this.#balance;
}
withdraw(amount, pin) {
if (!this.#validatePin(pin)) throw new Error('PIN 错误');
if (amount > this.#balance) throw new Error('余额不足');
this.#balance -= amount;
return this.#balance;
}
get balance() {
return this.#balance;
}
// 检查某对象是否有某私有字段(ES2022 in 操作符用法)
static hasBalance(obj) {
return #balance in obj;
}
}
const account = new BankAccount(100, '1234');
account.deposit(50); // 150
// account.#balance; // SyntaxError: Private field must be declared in an enclosing class
console.log(account.balance); // 150(通过 getter 只读访问)
console.log(BankAccount.hasBalance(account)); // truestatic 静态初始化块(ES2022):
static 块允许在类定义阶段执行复杂的静态初始化逻辑,可以访问私有静态字段,适合需要多步骤初始化的场景。
class Config {
static #settings = {};
static environment;
// 静态初始化块,在类加载时执行一次
static {
const env = typeof process !== 'undefined' ? 'node' : 'browser';
this.environment = env;
this.#settings = {
env,
timestamp: Date.now(),
features: env === 'node' ? ['fs', 'net'] : ['dom', 'fetch'],
};
}
static get settings() {
return { ...this.#settings };
}
}
console.log(Config.environment); // 'node' 或 'browser'
console.log(Config.settings.features);真实案例:用私有字段封装状态管理器
class Store {
#state;
#listeners = new Set();
constructor(initialState = {}) {
this.#state = initialState;
}
getState() {
return { ...this.#state }; // 返回副本,防止外部直接修改
}
setState(partial) {
this.#state = { ...this.#state, ...partial };
this.#notify();
}
subscribe(listener) {
this.#listeners.add(listener);
return () => this.#listeners.delete(listener); // 返回取消订阅函数
}
#notify() {
this.#listeners.forEach((fn) => fn(this.getState()));
}
}
const store = new Store({ count: 0 });
const unsubscribe = store.subscribe((state) => console.log('新状态:', state));
store.setState({ count: 1 }); // 触发监听器
unsubscribe();ES Module 模块系统
ES Module(简称 ESM)是 ECMAScript 官方的模块化方案,使用 import 和 export 关键字。它是静态的、支持 tree-shaking(摇树优化)的模块系统,取代了社区方案 CommonJS(Node.js 的 require)和 AMD。
为什么重要: 在 ESM 之前,浏览器端没有原生模块系统,只能通过全局变量、IIFE 或打包工具解决依赖管理问题。ESM 让 JavaScript 拥有了标准的、跨平台的模块化能力。
原理: ESM 是静态解析的——import/export 必须在模块顶层,引擎在编译阶段就能确定模块依赖关系,从而支持 tree-shaking(移除未使用的导出)和循环依赖处理。
// ===== 命名导出(named export)=====
// math.js
export const PI = 3.14159;
export function add(a, b) {
return a + b;
}
export class Calculator {}
// 或集中导出
const subtract = (a, b) => a - b;
const multiply = (a, b) => a * b;
export { subtract, multiply };
// ===== 默认导出(default export)=====
// logger.js
export default function log(message) {
console.log(`[LOG] ${message}`);
}
// ===== 导入 =====
import log from './logger.js'; // 默认导入
import { PI, add } from './math.js'; // 命名导入
import { subtract as minus } from './math.js'; // 重命名导入
import * as math from './math.js'; // 命名空间导入
import defaultExport, { PI as pi } from './math.js'; // 混合导入
// ===== 重新导出(re-export,常用于 index.js 聚合)=====
export { add, subtract } from './math.js';
export { default as Logger } from './logger.js';
export * from './utils.js';动态 import(ES2020):
静态 import 必须在顶层,而动态 import() 是一个返回 Promise 的函数式语法,可以在任意位置按需加载模块,是实现代码分割(code splitting)和懒加载的核心。
// 按需加载:只有用户点击时才加载重型模块
button.addEventListener('click', async () => {
const { renderChart } = await import('./heavy-chart.js');
renderChart(data);
});
// 条件加载:根据环境加载不同实现
async function loadPolyfill() {
if (!('IntersectionObserver' in window)) {
await import('intersection-observer');
}
}
// 并行加载多个模块
const [moduleA, moduleB] = await Promise.all([
import('./a.js'),
import('./b.js'),
]);Top-level await(ES2022):
在 ES2022 之前,await 只能用在 async 函数内部。Top-level await 允许在模块顶层直接使用 await,让模块可以异步初始化。
// config.js(作为 ES Module)
// 顶层 await:模块加载会等待这个 Promise 完成
const response = await fetch('/api/config');
export const config = await response.json();
// 动态选择依赖
const strings = await (
process.env.LANG === 'zh' ? import('./zh.js') : import('./en.js')
);
// 导入此模块的其他模块会等待其顶层 await 完成后才继续执行常见坑: 顶层 await 会阻塞模块图中依赖它的模块的执行,可能拖慢应用启动。应仅用于确实必要的初始化。此外,CommonJS 与 ESM 混用时存在互操作限制(如 CJS 不能直接 import ESM 的顶层 await 模块)。
可选链 ?.(ES2020)
可选链操作符 ?. 允许安全地访问嵌套对象的深层属性,当链中某一环为 null 或 undefined 时,表达式短路返回 undefined,而不会抛出 "Cannot read property of undefined" 错误。
为什么重要: 访问 API 返回的深层嵌套数据时,任何一层缺失都会导致运行时报错。传统写法需要层层 && 判断,冗长且难读。
const user = {
profile: {
address: { city: 'Beijing' },
},
};
// 传统写法:层层判断
const city1 = user && user.profile && user.profile.address && user.profile.address.city;
// 可选链写法
const city2 = user?.profile?.address?.city; // 'Beijing'
const zip = user?.profile?.address?.zipCode; // undefined(不报错)
// 可选链调用方法
user?.getName?.(); // 方法存在才调用
// 可选链访问数组元素
const first = user?.friends?.[0];
// 与空值合并配合(见下节)
const displayCity = user?.profile?.address?.city ?? '未知城市';常见坑: 可选链只对 null 和 undefined 短路,对空字符串、0、false 不短路。另外不要滥用——如果某个属性逻辑上一定存在,用可选链反而会掩盖真正的 bug。
空值合并 ??(ES2020)
空值合并操作符 ?? 返回左侧操作数,当左侧为 null 或 undefined 时返回右侧操作数。它与逻辑或 || 的关键区别是:|| 对所有假值(0、''、false、NaN)都返回右侧,而 ?? 只对 null/undefined 生效。
// || 的问题:0 和空字符串被误判
const count1 = 0 || 10; // 10(可能不是想要的)
const name1 = '' || 'default'; // 'default'(可能不是想要的)
// ?? 只在 null/undefined 时生效
const count2 = 0 ?? 10; // 0(保留了合法的 0)
const name2 = '' ?? 'default'; // ''(保留了合法的空字符串)
const value = null ?? 'default'; // 'default'
const value2 = undefined ?? 'default'; // 'default'
// 真实案例:处理配置默认值
function setup(options) {
const timeout = options.timeout ?? 3000; // 允许 timeout 为 0
const retries = options.retries ?? 3;
const verbose = options.verbose ?? false; // 允许显式 false
return { timeout, retries, verbose };
}
setup({ timeout: 0, verbose: false }); // { timeout: 0, retries: 3, verbose: false }
// ?? 不能直接与 && 或 || 混用,需加括号
// const x = a || b ?? c; // SyntaxError
const x = (true || false) ?? 'c'; // 需要括号明确优先级逻辑赋值运算符(ES2021)
ES2021 引入了三个逻辑赋值运算符:&&=、||=、??=,它们将逻辑运算与赋值结合,是短路求值的语法糖。
注意: 这三者都是短路赋值,只有满足条件时才执行赋值操作(而非无条件赋值),因此不会触发 setter 或多余的写操作。
// ||= 设置默认值
let config = { timeout: 0 };
config.retries ||= 3; // retries 不存在,赋值为 3
config.timeout ||= 5000; // timeout 为 0(假值),被覆盖为 5000(可能是 bug!)
// ??= 更安全地设置默认值(推荐用于默认值场景)
let settings = { timeout: 0 };
settings.retries ??= 3; // 赋值为 3
settings.timeout ??= 5000; // timeout 为 0(非 null),保持 0
// &&= 条件更新
let user = { name: 'Alice', token: 'abc' };
user.token &&= refreshToken(user.token); // token 存在才刷新
function refreshToken(t) {
return t + '-refreshed';
}
// 真实案例:用 ??= 简化缓存初始化
const cache = {};
function getData(key) {
cache[key] ??= expensiveComputation(key); // 只在首次计算
return cache[key];
}
function expensiveComputation(k) {
return `computed-${k}`;
}数组新方法
Array.flat 和 flatMap(ES2019):
flat 将嵌套数组"拉平",可指定深度;flatMap 相当于 map 后再 flat(1),常用于一对多的映射场景。
// flat:拉平嵌套数组
const nested = [1, [2, [3, [4]]]];
nested.flat(); // [1, 2, [3, [4]]](默认深度 1)
nested.flat(2); // [1, 2, 3, [4]]
nested.flat(Infinity); // [1, 2, 3, 4](完全拉平)
// flat 可用于去除空位
[1, , 3, , 5].flat(); // [1, 3, 5]
// flatMap:映射 + 拉平一层
const sentences = ['Hello world', 'Foo bar'];
sentences.flatMap((s) => s.split(' ')); // ['Hello', 'world', 'Foo', 'bar']
// flatMap 实现 filter + map 的组合(返回空数组等于过滤)
const numbers = [1, 2, 3, 4];
numbers.flatMap((n) => (n % 2 === 0 ? [n * 10] : [])); // [20, 40]Array.at(ES2022):
at() 方法支持负数索引,方便从数组末尾访问元素。
const arr = [10, 20, 30, 40, 50];
arr.at(0); // 10
arr.at(-1); // 50(最后一个元素)
arr.at(-2); // 40
// 对比传统写法
arr[arr.length - 1]; // 50(冗长)
// 字符串也支持 at
'hello'.at(-1); // 'o'findLast 和 findLastIndex(ES2023):
从数组末尾开始查找,与 find/findIndex 方向相反。
const nums = [1, 2, 3, 4, 5, 6];
nums.findLast((n) => n % 2 === 0); // 6(最后一个偶数)
nums.findLastIndex((n) => n % 2 === 0); // 5(索引)不可变数组方法 toSorted / toReversed / toSpliced / with(ES2023):
这些方法返回新数组,不修改原数组,解决了 sort/reverse/splice 会原地修改的痛点,非常适合函数式和响应式编程(如 React 状态更新)。
const original = [3, 1, 2];
// toSorted:返回排序后的新数组(原数组不变)
const sorted = original.toSorted(); // [1, 2, 3]
console.log(original); // [3, 1, 2](未变)
// toReversed:返回反转后的新数组
const reversed = original.toReversed(); // [2, 1, 3]
// toSpliced:返回增删后的新数组
const spliced = original.toSpliced(1, 1, 99); // [3, 99, 2]
// with:返回替换某索引后的新数组
const replaced = original.with(0, 100); // [100, 1, 2]
// 对比传统的原地修改(React 中会导致状态未更新的 bug)
// original.sort(); // 修改了原数组!对象新方法
Object.fromEntries(ES2019):
与 Object.entries 相反,将键值对列表(数组或 Map)转换为对象。
// 从二维数组构建对象
const entries = [['name', 'Alice'], ['age', 25]];
Object.fromEntries(entries); // { name: 'Alice', age: 25 }
// 从 Map 转对象
const map = new Map([['a', 1], ['b', 2]]);
Object.fromEntries(map); // { a: 1, b: 2 }
// 真实案例:处理 URLSearchParams(查询字符串转对象)
const params = new URLSearchParams('page=2&size=10&sort=desc');
const query = Object.fromEntries(params);
console.log(query); // { page: '2', size: '10', sort: 'desc' }
// 真实案例:转换对象的值(配合 entries + map)
const prices = { apple: 1, banana: 2 };
const discounted = Object.fromEntries(
Object.entries(prices).map(([k, v]) => [k, v * 0.9])
);
console.log(discounted); // { apple: 0.9, banana: 1.8 }
// 真实案例:过滤对象属性
const data = { a: 1, b: null, c: 3, d: undefined };
const cleaned = Object.fromEntries(
Object.entries(data).filter(([, v]) => v != null)
);
console.log(cleaned); // { a: 1, c: 3 }Object.hasOwn(ES2022):
Object.hasOwn(obj, key) 是 Object.prototype.hasOwnProperty.call(obj, key) 的安全替代,避免了对象自身重写 hasOwnProperty 或原型为 null 时的问题。
const obj = { name: 'Alice' };
Object.hasOwn(obj, 'name'); // true
Object.hasOwn(obj, 'toString'); // false(继承的不算)
// 为什么比 hasOwnProperty 安全
const tricky = Object.create(null); // 原型为 null,没有 hasOwnProperty
tricky.key = 1;
// tricky.hasOwnProperty('key'); // TypeError
Object.hasOwn(tricky, 'key'); // true(安全)Object.groupBy(ES2024):
按回调返回的键对数组元素分组,返回一个普通对象。相关的 Map.groupBy 返回 Map。这个特性早期以 Array.group 提案形式讨论,最终以 Object.groupBy / Map.groupBy 的形式在 ES2024 落地。
const items = [
{ name: 'apple', type: 'fruit' },
{ name: 'carrot', type: 'veg' },
{ name: 'banana', type: 'fruit' },
];
// Object.groupBy(ES2024)
const grouped = Object.groupBy(items, (item) => item.type);
// { fruit: [{apple}, {banana}], veg: [{carrot}] }
// Map.groupBy(键可以是任意类型,包括对象)
const byType = Map.groupBy(items, (item) => item.type);Promise 相关特性
Promise.allSettled(ES2020):
等待所有 Promise 完成(无论成功或失败),返回每个结果的状态。与 Promise.all 不同,它不会因为某个 Promise 失败而整体拒绝。
const promises = [
Promise.resolve('成功1'),
Promise.reject('失败'),
Promise.resolve('成功2'),
];
const results = await Promise.allSettled(promises);
// [
// { status: 'fulfilled', value: '成功1' },
// { status: 'rejected', reason: '失败' },
// { status: 'fulfilled', value: '成功2' }
// ]
// 真实案例:批量请求,即使部分失败也要处理成功的
const urls = ['/api/a', '/api/b', '/api/c'];
const responses = await Promise.allSettled(urls.map((u) => fetch(u)));
const succeeded = responses
.filter((r) => r.status === 'fulfilled')
.map((r) => r.value);Promise.any(ES2021):
返回第一个成功的 Promise,只有当所有 Promise 都失败时才拒绝(抛出 AggregateError)。与 Promise.race 不同,race 返回第一个完成的(无论成败)。
// 从多个镜像源中取最快成功的
const mirrors = [
fetch('https://mirror1.example.com/data'),
fetch('https://mirror2.example.com/data'),
fetch('https://mirror3.example.com/data'),
];
try {
const fastest = await Promise.any(mirrors);
console.log('最快的响应:', fastest);
} catch (err) {
// 所有都失败
console.error(err instanceof AggregateError); // true
console.error(err.errors); // 所有错误的数组
}Promise.withResolvers(ES2024):
返回一个包含 promise、resolve、reject 的对象,避免了将 resolve/reject 提取到外部作用域的样板代码。
// 传统写法
let resolveOuter, rejectOuter;
const p1 = new Promise((resolve, reject) => {
resolveOuter = resolve;
rejectOuter = reject;
});
// ES2024 写法
const { promise, resolve, reject } = Promise.withResolvers();
someEmitter.on('done', resolve);
someEmitter.on('error', reject);
await promise;Promise.all / race 回顾对比:
| 方法 | 何时兑现 | 何时拒绝 | 引入版本 |
| --- | --- | --- | --- |
| Promise.all | 全部成功 | 任一失败即拒绝 | ES2015 |
| Promise.race | 第一个完成(成功) | 第一个完成(失败) | ES2015 |
| Promise.allSettled | 全部完成 | 从不拒绝 | ES2020 |
| Promise.any | 第一个成功 | 全部失败(AggregateError) | ES2021 |
字符串新方法
String.padStart / padEnd(ES2017):
用指定字符填充字符串到目标长度,常用于对齐、补零。
'5'.padStart(3, '0'); // '005'(补零)
'5'.padEnd(3, '0'); // '500'
'42'.padStart(5); // ' 42'(默认空格填充)
// 真实案例:格式化时间
const h = 9, m = 5;
`${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`; // '09:05'
// 真实案例:脱敏显示银行卡号
const card = '6222021234567890';
card.slice(-4).padStart(card.length, '*'); // '************7890'String.replaceAll(ES2021):
替换字符串中所有匹配项,无需像 replace 那样使用带 g 标志的正则。
const text = 'a-b-c-d';
text.replaceAll('-', '_'); // 'a_b_c_d'
// 传统写法需要正则
text.replace(/-/g, '_'); // 'a_b_c_d'
// 支持回调
'x1y2z3'.replaceAll(/\d/g, (m) => `[${m}]`); // 'x[1]y[2]z[3]'String.trimStart / trimEnd(ES2019):
' hello '.trimStart(); // 'hello '
' hello '.trimEnd(); // ' hello'String.at(ES2022)与 matchAll(ES2020):
'hello'.at(-1); // 'o'
// matchAll 返回所有匹配的迭代器(含捕获组)
const str = '2023-01, 2024-02';
const matches = [...str.matchAll(/(\d{4})-(\d{2})/g)];
matches.forEach((m) => console.log(m[1], m[2])); // '2023' '01' / '2024' '02'数字与 BigInt
数字分隔符(ES2021):
可以用下划线 _ 作为数字字面量的分隔符,提高大数字的可读性,不影响数值。
const million = 1_000_000; // 等于 1000000
const bytes = 0xFF_FF_FF; // 十六进制也可用
const binary = 0b1010_0001; // 二进制
const price = 19_999.99;BigInt(ES2020):
BigInt 是一种可以表示任意精度整数的原始类型,解决了 Number 类型只能安全表示 2^53 - 1 以内整数的限制。在字面量后加 n 或调用 BigInt() 创建。
// Number 的精度上限
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
console.log(9007199254740992 === 9007199254740993); // true(精度丢失!)
// BigInt 解决大整数问题
const big = 9007199254740993n;
const big2 = BigInt('9007199254740993');
console.log(big + 1n); // 9007199254740994n
// BigInt 不能与 Number 直接混合运算
// 1n + 1; // TypeError
console.log(1n + BigInt(1)); // 2n
console.log(Number(1n) + 1); // 2
// 真实案例:处理雪花 ID、大额金融数字、时间戳纳秒
const snowflakeId = 1234567890123456789n;常见坑: BigInt 不支持小数,不能用于 Math 对象的方法,JSON.stringify 无法直接序列化 BigInt(会抛错,需要自定义 replacer)。
Symbol
Symbol 是 ES6 引入的原始类型,表示唯一且不可变的值,常用作对象属性键以避免命名冲突,也用于定义对象的内部行为(well-known symbols)。
// 创建唯一 Symbol
const id1 = Symbol('id');
const id2 = Symbol('id');
console.log(id1 === id2); // false(每个都唯一)
// 作为对象属性键(不会与字符串键冲突,不会被常规遍历)
const user = {
name: 'Alice',
[id1]: 12345,
};
console.log(Object.keys(user)); // ['name'](Symbol 键不出现)
console.log(Object.getOwnPropertySymbols(user)); // [Symbol(id)]
// 全局 Symbol 注册表
const globalId = Symbol.for('app.id'); // 全局共享
console.log(Symbol.for('app.id') === globalId); // true
// Well-known Symbols:自定义迭代行为
const range = {
from: 1,
to: 3,
[Symbol.iterator]() {
let current = this.from;
const last = this.to;
return {
next() {
return current <= last
? { value: current++, done: false }
: { value: undefined, done: true };
},
};
},
};
console.log([...range]); // [1, 2, 3]
// Symbol.toPrimitive 自定义类型转换
const money = {
amount: 100,
[Symbol.toPrimitive](hint) {
return hint === 'string' ? `$${this.amount}` : this.amount;
},
};
console.log(`${money}`); // '$100'
console.log(money * 2); // 200集合类型:Map / Set / WeakMap / WeakSet / WeakRef
Map(ES2015):
Map 是键值对集合,键可以是任意类型(包括对象),且保持插入顺序。相比普通对象,Map 更适合频繁增删、键类型多样、需要保持顺序的场景。
const map = new Map();
map.set('name', 'Alice');
map.set(42, 'number key');
const objKey = { id: 1 };
map.set(objKey, 'object key'); // 对象可作为键
console.log(map.get(objKey)); // 'object key'
console.log(map.size); // 3
console.log(map.has('name')); // true
map.delete(42);
// 遍历(保持插入顺序)
for (const [key, value] of map) {
console.log(key, value);
}
// 从数组初始化
const scores = new Map([['math', 90], ['english', 85]]);Set(ES2015):
Set 是值的集合,自动去重,常用于数组去重和成员检测。
// 数组去重
const arr = [1, 2, 2, 3, 3, 3];
const unique = [...new Set(arr)]; // [1, 2, 3]
const set = new Set();
set.add(1);
set.add(1); // 重复,无效果
console.log(set.size); // 1
console.log(set.has(1)); // true
// 求交集、并集、差集
const a = new Set([1, 2, 3]);
const b = new Set([2, 3, 4]);
const intersection = new Set([...a].filter((x) => b.has(x))); // {2, 3}
const union = new Set([...a, ...b]); // {1, 2, 3, 4}
const difference = new Set([...a].filter((x) => !b.has(x))); // {1}WeakMap / WeakSet(ES2015):
键只能是对象,且是弱引用——当键对象没有其他引用时会被垃圾回收,不会造成内存泄漏。适合存储与对象关联的私有数据、缓存等。不可遍历,没有 size。
// 用 WeakMap 存储 DOM 节点的私有数据
const nodeData = new WeakMap();
function attachData(node, data) {
nodeData.set(node, data);
}
// 当 node 被移除且无其他引用时,对应数据自动被回收
// 用 WeakMap 实现真正的私有属性(ES2022 # 出现前的方案)
const privates = new WeakMap();
class Counter {
constructor() {
privates.set(this, { count: 0 });
}
increment() {
privates.get(this).count++;
}
get value() {
return privates.get(this).count;
}
}WeakRef 和 FinalizationRegistry(ES2021):
WeakRef 创建对对象的弱引用,允许访问对象但不阻止其被回收。FinalizationRegistry 可以在对象被回收后执行清理回调。这是高级特性,应谨慎使用。
// WeakRef:缓存但不阻止回收
let bigObject = { data: 'huge' };
const ref = new WeakRef(bigObject);
// 稍后访问
const obj = ref.deref(); // 可能返回对象,也可能返回 undefined(已回收)
if (obj) {
console.log(obj.data);
}
// FinalizationRegistry:对象回收后清理
const registry = new FinalizationRegistry((heldValue) => {
console.log(`对象 ${heldValue} 已被回收,执行清理`);
});
registry.register(bigObject, 'bigObject-id');Proxy 和 Reflect(ES2015)
Proxy 允许拦截并自定义对象的基本操作(读取、赋值、删除、函数调用等),是实现响应式系统(如 Vue 3)、数据校验、日志记录等的强大工具。Reflect 提供了与 Proxy 陷阱一一对应的默认操作方法。
// 基本 Proxy:拦截读写
const target = { name: 'Alice', age: 25 };
const proxy = new Proxy(target, {
get(obj, prop, receiver) {
console.log(`读取属性 ${String(prop)}`);
return Reflect.get(obj, prop, receiver);
},
set(obj, prop, value, receiver) {
if (prop === 'age' && typeof value !== 'number') {
throw new TypeError('age 必须是数字');
}
console.log(`设置 ${String(prop)} = ${value}`);
return Reflect.set(obj, prop, value, receiver);
},
});
proxy.name; // 打印 "读取属性 name",返回 'Alice'
proxy.age = 30; // 打印 "设置 age = 30"
// proxy.age = 'thirty'; // TypeError
// 真实案例:实现简易响应式(Vue 3 原理简化版)
function reactive(obj, onChange) {
return new Proxy(obj, {
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
onChange(key, value);
return result;
},
});
}
const state = reactive({ count: 0 }, (key, value) => {
console.log(`${key} 变为 ${value},触发视图更新`);
});
state.count = 1; // 打印 "count 变为 1,触发视图更新"
// 真实案例:默认值对象
function withDefault(defaultValue) {
return new Proxy({}, {
get(target, prop) {
return prop in target ? target[prop] : defaultValue;
},
});
}
const scores = withDefault(0);
console.log(scores.math); // 0(不存在返回默认值)Generator 与迭代器(ES2015)
Generator 函数(function*)可以暂停和恢复执行,通过 yield 逐步产出值。它是实现自定义迭代器、惰性求值、协程式异步流程的基础。
// 基本 Generator
function* counter() {
let i = 0;
while (true) {
yield i++;
}
}
const gen = counter();
console.log(gen.next().value); // 0
console.log(gen.next().value); // 1
// 有限 Generator
function* range(start, end, step = 1) {
for (let i = start; i < end; i += step) {
yield i;
}
}
console.log([...range(0, 10, 2)]); // [0, 2, 4, 6, 8]
// yield* 委托给另一个可迭代对象
function* combined() {
yield* [1, 2];
yield* range(10, 13);
}
console.log([...combined()]); // [1, 2, 10, 11, 12]
// 双向通信:next 可以传值回 Generator
function* dialog() {
const name = yield '你叫什么名字?';
yield `你好,${name}!`;
}
const d = dialog();
console.log(d.next().value); // '你叫什么名字?'
console.log(d.next('Alice').value); // '你好,Alice!'
// 异步迭代器(ES2018):for await...of
async function* fetchPages(urls) {
for (const url of urls) {
const res = await fetch(url);
yield res.json();
}
}
// for await (const page of fetchPages(urls)) { console.log(page); }for...of 与迭代协议(ES2015)
for...of 遍历可迭代对象(数组、字符串、Map、Set、arguments、NodeList 等)的值,比 for...in(遍历键)和传统 for 更简洁安全。
// 遍历数组值
for (const value of [10, 20, 30]) {
console.log(value); // 10, 20, 30
}
// 配合 entries 获取索引
for (const [index, value] of ['a', 'b'].entries()) {
console.log(index, value); // 0 'a' / 1 'b'
}
// 遍历字符串(按 Unicode 码点)
for (const char of '你好') {
console.log(char); // '你' / '好'
}
// for...of vs for...in 对比
const arr = ['a', 'b'];
arr.customProp = 'x';
for (const i in arr) console.log(i); // '0', '1', 'customProp'(遍历键+继承属性)
for (const v of arr) console.log(v); // 'a', 'b'(只遍历值,不含额外属性)async / await(ES2017)与异步演进
async/await 是基于 Promise 的语法糖,让异步代码看起来像同步代码,极大提升了可读性和错误处理体验。它是 JavaScript 异步编程从回调地狱到 Promise 再到 async/await 演进的终点。
// 基本用法
async function fetchUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const user = await response.json();
return user;
} catch (error) {
console.error('获取用户失败:', error);
throw error;
}
}
// 并行执行(不要串行 await 无依赖的请求)
// 错误示范(串行,慢):
async function slow() {
const a = await fetch('/a'); // 等 a
const b = await fetch('/b'); // 再等 b
return [a, b];
}
// 正确示范(并行,快):
async function fast() {
const [a, b] = await Promise.all([fetch('/a'), fetch('/b')]);
return [a, b];
}
// 在循环中串行处理
async function processSequentially(items) {
const results = [];
for (const item of items) {
results.push(await process(item)); // 逐个等待
}
return results;
}
function process(item) {
return Promise.resolve(item * 2);
}异步方案对比:
| 方案 | 引入 | 优点 | 缺点 |
| --- | --- | --- | --- |
| 回调函数 | 远古 | 简单直接 | 回调地狱、错误处理难 |
| Promise | ES2015 | 链式调用、统一错误处理 | .then 嵌套仍显冗长 |
| async/await | ES2017 | 同步式写法、易读易调试 | 需注意并行优化 |
structuredClone(深拷贝)
structuredClone 是较新的全局函数(并非 ECMAScript 语言标准,而是 HTML/WHATWG 规范,但现代 Node.js 与浏览器均已内置),用于对对象进行深拷贝,支持循环引用、Map、Set、Date、ArrayBuffer 等,弥补了 JSON.parse(JSON.stringify()) 的诸多缺陷。
const original = {
date: new Date(),
map: new Map([['key', 'value']]),
nested: { arr: [1, 2, 3] },
};
original.self = original; // 循环引用
const clone = structuredClone(original);
clone.nested.arr.push(4);
console.log(original.nested.arr); // [1, 2, 3](原对象未受影响)
// 对比 JSON 方式的缺陷
const bad = JSON.parse(JSON.stringify(original)); // 会丢失 Date 类型、无法处理循环引用(抛错)
// 局限:无法克隆函数、DOM 节点、Symbol
// structuredClone({ fn: () => {} }); // DataCloneError相等性比较:== vs === vs Object.is
理解相等性判断是避免隐蔽 bug 的关键。== 会进行类型转换(宽松相等),=== 不转换类型(严格相等),Object.is(ES2015)则处理了 === 的两个特殊情况。
// == 类型转换的陷阱
0 == ''; // true
0 == '0'; // true
'' == '0'; // false(不传递!)
null == undefined; // true
NaN == NaN; // false
[] == false; // true
[] == ![]; // true(诡异)
// === 严格相等(推荐默认使用)
0 === ''; // false
null === undefined; // false
NaN === NaN; // false
// Object.is:修正 === 的两个特例
Object.is(NaN, NaN); // true(=== 为 false)
Object.is(+0, -0); // false(=== 为 true)
Object.is(1, 1); // true== vs === 对比:
| 比较 | == 结果 | === 结果 | Object.is 结果 |
| --- | --- | --- | --- |
| 0 与 '' | true | false | false |
| null 与 undefined | true | false | false |
| NaN 与 NaN | false | false | true |
| +0 与 -0 | true | true | false |
| 1 与 '1' | true | false | false |
最佳实践: 始终使用 === 和 !==,仅在明确需要判断 null/undefined 时用 == null(同时匹配两者)。
指数运算符与 Array.includes(ES2016)
ES2016 是最"小"的一个版本,只增加了两个特性。
// 指数运算符 **(替代 Math.pow)
console.log(2 ** 10); // 1024
console.log(Math.pow(2, 10)); // 1024(旧写法)
let base = 3;
base **= 2; // 9(指数赋值)
// Array.includes(比 indexOf 更直观,且能检测 NaN)
const arr = [1, 2, NaN];
arr.includes(2); // true
arr.includes(NaN); // true(indexOf 无法检测 NaN)
arr.indexOf(NaN); // -1(检测不到)Object.values / Object.entries(ES2017)
const scores = { math: 90, english: 85, science: 95 };
Object.keys(scores); // ['math', 'english', 'science']
Object.values(scores); // [90, 85, 95]
Object.entries(scores); // [['math', 90], ['english', 85], ['science', 95]]
// 真实案例:计算平均分
const avg =
Object.values(scores).reduce((sum, s) => sum + s, 0) /
Object.values(scores).length;
// 真实案例:遍历对象
for (const [subject, score] of Object.entries(scores)) {
console.log(`${subject}: ${score}`);
}ES2018 补充:对象展开与正则改进
ES2018 将扩展运算符从数组扩展到对象(前面已用到),并增强了正则表达式能力。
// 对象 rest/spread(ES2018)
const { id, ...rest } = { id: 1, name: 'A', age: 20 };
console.log(rest); // { name: 'A', age: 20 }
// 具名捕获组
const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const match = '2024-03-15'.match(re);
console.log(match.groups.year); // '2024'
console.log(match.groups.month); // '03'
// 后行断言
const price = '$100'.match(/(?<=\$)\d+/);
console.log(price[0]); // '100'
// dotAll 模式(s 标志,让 . 匹配换行)
/foo.bar/s.test('foo\nbar'); // true
// Promise.finally(ES2018)
fetch('/api')
.then((res) => res.json())
.catch((err) => console.error(err))
.finally(() => console.log('无论成败都执行'));综合实战案例
下面把多个特性组合起来,展示它们在真实项目中的协作。
// 案例一:安全解析 API 响应
// 组合可选链、空值合并、解构、默认参数
function parseUserResponse(response) {
const {
data: {
user: {
profile: { displayName } = {},
settings: { theme } = {},
} = {},
} = {},
} = response ?? {};
return {
name: displayName ?? '匿名用户',
theme: theme ?? 'light',
avatar: response?.data?.user?.profile?.avatar?.url ?? '/default.png',
};
}
// 案例二:构建查询字符串工具
function buildQuery(params) {
const cleaned = Object.fromEntries(
Object.entries(params).filter(([, v]) => v != null && v !== '')
);
return new URLSearchParams(cleaned).toString();
}
buildQuery({ page: 1, size: 10, keyword: '', tag: null }); // 'page=1&size=10'
// 案例三:带缓存和默认值的配置管理器
class ConfigManager {
#cache = new Map();
#defaults;
constructor(defaults = {}) {
this.#defaults = defaults;
}
get(key) {
this.#cache.has(key) || this.#cache.set(key, this.#compute(key));
return this.#cache.get(key);
}
#compute(key) {
return this.#defaults[key] ?? null;
}
}
// 案例四:批量并发请求,容错处理
async function fetchAllUsers(ids) {
const results = await Promise.allSettled(
ids.map((id) => fetch(`/api/users/${id}`).then((r) => r.json()))
);
const users = results
.filter((r) => r.status === 'fulfilled')
.map((r) => r.value);
const failedCount = results.filter((r) => r.status === 'rejected').length;
console.log(`成功 ${users.length} 个,失败 ${failedCount} 个`);
return users;
}
// 案例五:数据分组与统计(ES2024 groupBy)
const orders = [
{ id: 1, status: 'paid', amount: 100 },
{ id: 2, status: 'pending', amount: 50 },
{ id: 3, status: 'paid', amount: 200 },
];
const byStatus = Object.groupBy(orders, (o) => o.status);
const paidTotal = (byStatus.paid ?? []).reduce((sum, o) => sum + o.amount, 0);
console.log(`已支付总额: ${paidTotal}`); // 300最佳实践
代码风格:
性能考虑:
兼容性:
可读性与安全性:
常见坑汇总
| 陷阱 | 说明 | 解决方案 |
| --- | --- | --- |
| 箭头函数的 this | 无法动态绑定,call/apply/bind 无效 | 需要动态 this 时用普通函数 |
| || 处理默认值 | 0、''、false 被误判为需要默认值 | 用 ?? 只对 null/undefined 生效 |
| 扩展运算符浅拷贝 | 嵌套对象仍是引用共享 | 用 structuredClone 深拷贝 |
| const 不等于不可变 | 只保证绑定不变 | 需要不可变用 Object.freeze |
| BigInt 与 Number 混用 | 直接运算抛 TypeError | 显式转换类型 |
| 串行 await | 无依赖请求串行导致慢 | 用 Promise.all 并行 |
| == 类型转换 | 隐式转换产生意外结果 | 始终用 === |
| sort/reverse 原地修改 | 意外修改原数组 | 用 toSorted/toReversed |
实际应用场景
前端开发:
Node.js 开发:
总结
从 ES2015 到 ES2024,ECMAScript 每年的更新都在让 JavaScript 变得更强大、更易用。ES6 奠定了现代 JavaScript 的基础(let/const、箭头函数、Class、Module、Promise、解构、模板字符串);此后的年度版本则持续打磨异步能力(async/await、Promise.allSettled/any)、增强安全访问(可选链、空值合并、逻辑赋值)、丰富内置方法(数组/对象/字符串新方法)、完善封装(私有字段、static 块),并引入不可变操作等函数式友好特性。
掌握这些特性不仅能写出更简洁、更健壮的代码,更重要的是理解每个特性背后要解决的问题,从而在合适的场景选择合适的工具。
各版本核心特性汇总:
| 版本 | 年份 | 核心特性 |
| --- | --- | --- |
| ES6 | 2015 | let/const、箭头函数、模板字符串、解构、默认参数、rest/spread、Class、Module、Promise、Map/Set/WeakMap、Symbol、Generator、for...of、Proxy/Reflect |
| ES2016 | 2016 | 指数运算符 **、Array.includes |
| ES2017 | 2017 | async/await、Object.values/entries、String.padStart/padEnd |
| ES2018 | 2018 | 对象 rest/spread、异步迭代、正则具名捕获/后行断言、Promise.finally |
| ES2019 | 2019 | Array.flat/flatMap、Object.fromEntries、trimStart/End、可选 catch 绑定 |
| ES2020 | 2020 | 可选链 ?.、空值合并 ??、BigInt、动态 import、Promise.allSettled、matchAll、globalThis |
| ES2021 | 2021 | 逻辑赋值 &&=/||=/??=、数字分隔符、replaceAll、Promise.any、WeakRef |
| ES2022 | 2022 | Top-level await、私有字段 #、static 块、Array.at、Object.hasOwn、error cause |
| ES2023 | 2023 | findLast/findLastIndex、toSorted/toReversed/toSpliced/with、Hashbang 语法 |
| ES2024 | 2024 | Object.groupBy/Map.groupBy、Promise.withResolvers、ArrayBuffer 可调整大小 |