ECMAScript 新特性与最佳实践

中等 🟡Js/Ts
9 个标签
预计阅读时间:86 分钟
JavaScriptES6+新特性语法ES2015ES2020ES2022ES2023最佳实践

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 0(Strawperson,稻草人):初步想法,尚未正式提交。
Stage 1(Proposal,提案):正式提案,明确要解决的问题和大致 API 形态。
Stage 2(Draft,草案):确定语法和语义,开始编写规范文本。
Stage 3(Candidate,候选):规范基本完成,等待浏览器和引擎实现反馈。
Stage 4(Finished,完成):已被验证并纳入下一年的正式标准。

为什么重要: 处于 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。

javascriptCode
// 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 declared

var / 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) 或传入第二个参数等方式解决,代码冗长且容易出错。箭头函数从语法层面解决了这个问题。

javascriptCode
// 传统函数
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)则允许用函数处理模板字符串的各个部分,实现更强大的字符串处理逻辑。

javascriptCode
// 基本用法
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, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;');
  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 时应使用参数化查询而非字符串拼接。

解构赋值:

解构赋值允许从数组或对象中提取值,并赋给变量。解构赋值可以简化代码,使数据提取更加直观。支持数组解构、对象解构、函数参数解构等多种形式。

javascriptCode
// 数组解构
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 不会。

javascriptCode
// 基本默认参数
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 只是类数组对象。

javascriptCode
// 剩余参数
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 提供了统一、清晰的语法。

javascriptCode
// 基本类定义
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 调用父类构造函数和方法。

javascriptCode
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)。

为什么重要: 在 # 出现之前,"私有"属性只能通过 _ 前缀约定或闭包模拟,前者无强制性、后者写法繁琐。# 提供了引擎层面强制的封装。

javascriptCode
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)); // true

static 静态初始化块(ES2022):

static 块允许在类定义阶段执行复杂的静态初始化逻辑,可以访问私有静态字段,适合需要多步骤初始化的场景。

javascriptCode
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);

真实案例:用私有字段封装状态管理器

javascriptCode
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(移除未使用的导出)和循环依赖处理。

javascriptCode
// ===== 命名导出(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)和懒加载的核心。

javascriptCode
// 按需加载:只有用户点击时才加载重型模块
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,让模块可以异步初始化。

javascriptCode
// 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 返回的深层嵌套数据时,任何一层缺失都会导致运行时报错。传统写法需要层层 && 判断,冗长且难读。

javascriptCode
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 生效。

javascriptCode
// || 的问题: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 引入了三个逻辑赋值运算符:&&=、||=、??=,它们将逻辑运算与赋值结合,是短路求值的语法糖。

a ||= b 等价于 a || (a = b):a 为假值时才赋值。
a &&= b 等价于 a && (a = b):a 为真值时才赋值。
a ??= b 等价于 a ?? (a = b):a 为 null/undefined 时才赋值。

注意: 这三者都是短路赋值,只有满足条件时才执行赋值操作(而非无条件赋值),因此不会触发 setter 或多余的写操作。

javascriptCode
// ||= 设置默认值
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),常用于一对多的映射场景。

javascriptCode
// 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() 方法支持负数索引,方便从数组末尾访问元素。

javascriptCode
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 方向相反。

javascriptCode
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 状态更新)。

javascriptCode
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)转换为对象。

javascriptCode
// 从二维数组构建对象
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 时的问题。

javascriptCode
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 落地。

javascriptCode
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 失败而整体拒绝。

javascriptCode
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 返回第一个完成的(无论成败)。

javascriptCode
// 从多个镜像源中取最快成功的
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 提取到外部作用域的样板代码。

javascriptCode
// 传统写法
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):

用指定字符填充字符串到目标长度,常用于对齐、补零。

javascriptCode
'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 标志的正则。

javascriptCode
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):

javascriptCode
'   hello   '.trimStart(); // 'hello   '
'   hello   '.trimEnd(); // '   hello'

String.at(ES2022)与 matchAll(ES2020):

javascriptCode
'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):

可以用下划线 _ 作为数字字面量的分隔符,提高大数字的可读性,不影响数值。

javascriptCode
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() 创建。

javascriptCode
// 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)。

javascriptCode
// 创建唯一 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 更适合频繁增删、键类型多样、需要保持顺序的场景。

javascriptCode
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 是值的集合,自动去重,常用于数组去重和成员检测。

javascriptCode
// 数组去重
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。

javascriptCode
// 用 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 可以在对象被回收后执行清理回调。这是高级特性,应谨慎使用。

javascriptCode
// 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 陷阱一一对应的默认操作方法。

javascriptCode
// 基本 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 逐步产出值。它是实现自定义迭代器、惰性求值、协程式异步流程的基础。

javascriptCode
// 基本 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 更简洁安全。

javascriptCode
// 遍历数组值
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 演进的终点。

javascriptCode
// 基本用法
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()) 的诸多缺陷。

javascriptCode
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)则处理了 === 的两个特殊情况。

javascriptCode
// == 类型转换的陷阱
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 是最"小"的一个版本,只增加了两个特性。

javascriptCode
// 指数运算符 **(替代 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)

javascriptCode
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 将扩展运算符从数组扩展到对象(前面已用到),并增强了正则表达式能力。

javascriptCode
// 对象 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('无论成败都执行'));

综合实战案例

下面把多个特性组合起来,展示它们在真实项目中的协作。

javascriptCode
// 案例一:安全解析 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

最佳实践

代码风格:

默认使用 const,需要重新赋值时用 let,杜绝 var。
优先使用箭头函数处理回调,但对象方法、需要动态 this 的场景用普通函数。
使用模板字符串代替字符串拼接,注意 XSS 转义。
合理使用解构赋值提取数据,但避免过度嵌套解构降低可读性。
用可选链和空值合并简化防御性代码,但不要用它们掩盖本不该缺失的数据。

性能考虑:

无依赖的异步操作用 Promise.all 并行,不要串行 await。
避免在热点循环中滥用扩展运算符(每次都会创建新数组/对象)。
大整数、雪花 ID 等场景用 BigInt,但注意其运算不能与 Number 混用且性能低于 Number。
频繁增删、键类型多样时用 Map/Set 代替普通对象。

兼容性:

使用 Babel 转译新语法到目标环境支持的版本。
通过 core-js 等 polyfill 补齐运行时 API(如 Promise.any、Array.flat)。
用 browserslist 明确目标浏览器,配合 @babel/preset-env 按需转译。
关注 caniuse 和 Node.js 版本对应的特性支持情况。

可读性与安全性:

保持代码简洁,使用有意义的变量名,适当注释复杂逻辑。
注意模板字符串拼接 HTML/SQL 的注入风险,使用转义或参数化查询。
避免使用 eval 和 with。
用 # 私有字段实现真正的封装,而非 _ 命名约定。

常见坑汇总

| 陷阱 | 说明 | 解决方案 |

| --- | --- | --- |

| 箭头函数的 this | 无法动态绑定,call/apply/bind 无效 | 需要动态 this 时用普通函数 |

| || 处理默认值 | 0、''、false 被误判为需要默认值 | 用 ?? 只对 null/undefined 生效 |

| 扩展运算符浅拷贝 | 嵌套对象仍是引用共享 | 用 structuredClone 深拷贝 |

| const 不等于不可变 | 只保证绑定不变 | 需要不可变用 Object.freeze |

| BigInt 与 Number 混用 | 直接运算抛 TypeError | 显式转换类型 |

| 串行 await | 无依赖请求串行导致慢 | 用 Promise.all 并行 |

| == 类型转换 | 隐式转换产生意外结果 | 始终用 === |

| sort/reverse 原地修改 | 意外修改原数组 | 用 toSorted/toReversed |

实际应用场景

前端开发:

组件开发:Class 组件、解构 props、可选链访问嵌套 state。
状态管理:Proxy 实现响应式(Vue 3)、不可变数组方法更新状态(React)。
异步操作:async/await 请求数据、Promise.all 并行加载、动态 import 代码分割。
工具函数:解构、扩展运算符、Object.fromEntries 处理数据转换。

Node.js 开发:

服务器端代码:ES Module、Top-level await 初始化配置。
API 开发:async/await 处理请求、Promise.allSettled 批量调用下游服务。
脚本编写:数字分隔符提高常量可读性、Map/Set 处理数据集合。
工具开发:Generator 实现流式处理、WeakMap 缓存。

总结

从 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 可调整大小 |

学习资源

MDN Web Docs(权威 API 参考)
ECMAScript 规范(TC39 官方规范文档)
TC39 提案仓库(github.com/tc39/proposals,追踪各阶段提案)
JavaScript.info(系统教程)
caniuse.com(浏览器兼容性查询)
Node.green(Node.js 特性支持查询)
前端技术博客与开源项目源码