06 - ES6+ 特性

前置:异步编程。前几章已经零散用过不少新语法,本章系统盘点:哪些是 ES6 的里程碑特性,2016 年以后每年又补了什么好东西。


1. ES6 与年度发布节奏

ES6(官方名 ECMAScript 2015)是 JS 历史上最大的一次版本跃迁——let/const、class、模块、Promise、箭头函数等十余个核心特性同时落地,现代 JS 与”老 JS”的分界线就在这里。

此后规范改为年度发布:每年 6 月发布一版,特性按是否按时完成进入当年版本(ES2016、ES2017……ES2023 及以后)。这带来两个实际影响:

  • 不再存在”等大版本”的憋大招,好特性按年渐进落地
  • 学习时只需关注”哪一年加了什么”,浏览器支持情况查 caniuse 即可
  • 阅读老教程/老代码时注意甄别:出现 varrequire、回调风格的基本是 ES6 之前的写法

本教程环境默认现代浏览器,以下特性均可直接使用;老项目兼容方案见第 10 节 Babel 一段。


2. ES6 核心特性清单

前面章节已详细讲过的在此只做索引回顾:

  • let/const 与块级作用域JS 基础语法 第 3 节
  • 箭头函数与词法 this函数与作用域 第 4 节
  • 解构赋值 / 展开运算符对象与数组 第 3、4 节
  • 模板字符串 / 默认参数 / rest 参数:基础语法第 6 节、函数章第 2 节
  • class 类语法:对象与数组章第 6 节
  • Promise异步编程 全章(async/await 属于 ES2017)

下面补充尚未展开的重点项。

3. 对象字面量增强

ES6 让对象字面量的书写大幅瘦身,现代代码里随处可见:

const name = "购物车";
const items = ["苹果", "香蕉"];
 
// 属性简写:变量名即键名
const cart = { name, items };   // { name: "购物车", items: [...] }
 
// 方法简写:省略 function 关键字
const api = {
  fetch() { return "data"; },
  // 上一章 counter.increment() 的简写就是它
};
 
// 计算属性名:键可以是表达式
const field = "score";
const record = {
  [field]: 95,                    // { score: 95 }
  [`get${capitalize(field)}`]() {},
};

解构 + 简写组合出经典的”参数透传”模式:

function createRequest({ url, method = "GET", ...rest }) {
  console.log(url, method, rest);
}
createRequest({ url: "/api", method: "POST", body: "{}", timeout: 3000 });
// rest 自动收集多余参数 —— 解构与 rest 的合体用法

4. 模块 Module:import / export

3.1 基本用法

ES6 之前 JS 没有语言级模块机制,一个网页几十个 <script> 共享全局变量,命名冲突靠人肉规避。ES Module 终结了这一切:

// utils.js —— 导出方
export const PI = 3.14159;              // 具名导出
 
export function formatPrice(cents) {    // 具名导出
  return `¥${(cents / 100).toFixed(2)}`;
}
 
export default class Cart { ... }       // 默认导出:每个文件最多一个
// main.js —— 引入方
import Cart from "./cart.js";                    // 默认导出:名字随意取
import { PI, formatPrice } from "./utils.js";    // 具名导出:花括号且名字必须对上
import { formatPrice as fmt } from "./utils.js"; // 重命名
import * as utils from "./utils.js";             // 整体导入为命名空间

3.2 与 CommonJS 的对比

Node.js 传统上使用 CommonJS 规范(require),读老代码和 Node 项目必须认识它:

维度ESM(import/export)CommonJS(require)
加载时机编译期静态分析运行时动态 require
导入绑定实时只读引用值的拷贝
顶层 await支持不支持
浏览器原生支持(type="module"不支持
书写形式import { x } from "..."const { x } = require("...")
// 同一份功能的 CommonJS 写法
const { PI, formatPrice } = require("./utils");
module.exports = { PI, formatPrice };

趋势是全面 ESM 化:浏览器原生支持、打包工具优先优化、Node 新项目默认推荐。

3.3 浏览器中的 module script

<!-- module 脚本自动 defer,天然解决"DOM 未就绪"问题 -->
<script type="module" src="main.js"></script>
 
<!-- 内联模块也合法 -->
<script type="module">
  import { formatPrice } from "./utils.js";
</script>

模块文件各自形成独立作用域(顶层变量不再污染全局),且天然是一层闭包——上一章”模块私有变量”由此免费获得。

动态 import 返回 Promise,按需加载代码块:

button.addEventListener("click", async () => {
  const { openChart } = await import("./chart.js"); // 点击时才下载该模块
  openChart();
});

这是路由懒加载、首屏提速的基础机制,框架中的”代码分割”即源于此。

5. Map 与 Set

普通对象的键只能是字符串/Symbol;Map 支持任意类型键且自带 size、可迭代:

// Map:真正的字典结构,类似 java.util.HashMap
const cache = new Map();
cache.set({ id: 1 }, "对象也能当键");   // 对象键按引用比较
cache.set("count", 42);
console.log(cache.get("count"));        // 42
console.log(cache.has("count"));        // true
console.log(cache.size);                // 2
for (const [k, v] of cache) console.log(k, v); // 直接可迭代
cache.delete("count");
 
// Set:去重神器,类似 java.util.HashSet
const tags = new Set(["js", "web", "js"]);
console.log(tags.size);                          // 2
console.log([...new Set([1, 2, 2, 3])]);         // [1, 2, 3],数组去重惯用法

选择建议:键值集合用 Map 而非 {}(性能更好、语义更准),唯一性集合用 Set。WeakMap/WeakSet 是不阻止 GC 的弱引用变体,用于给 DOM 等外部对象附加元数据,了解即可。

6. class 的现代补充:静态成员与私有字段

对象与数组 章讲了基础语法,这里补齐 ES2022 后的完整形态:

class Counter {
  static instances = 0;        // 静态字段:属于类而非实例
  #secret = "私有字段";         // 私有字段:真私有,外部访问直接报错
  publicCount = 0;             // 公共实例字段
 
  constructor() {
    Counter.instances++;       // 统计创建了多少个实例
  }
 
  static compare(a, b) {       // 静态方法:类似 Java static 方法
    return a.publicCount - b.publicCount;
  }
 
  #log(msg) {                  // 私有方法
    console.log(`[内部] ${msg}`);
  }
 
  increment() {
    this.#log("自增");
    return ++this.publicCount;
  }
}
 
const c = new Counter();
console.log(Counter.instances);  // 1
c.increment();                   // 正常调用内部私有方法
// console.log(c.#secret);       // SyntaxError!类外不可见

#field 是语言级真私有(对比闭包方案和 _ 命名约定),语义上最接近 Java 的 private——这是 JS 向静态语言靠拢的又一例证。

7. Symbol 补充

Symbol 创建全局唯一的值,主要用途是作为”不会撞名的键”:

const AREA = Symbol("area");
const shape = {
  [AREA]: 100,          // 计算属性名 + symbol 键
  name: "矩形",
};
console.log(shape[AREA]);      // 100
console.log(Object.keys(shape)); // ["name"] —— symbol 键不出现在常规遍历中

库设计者用它定义协议常量(如迭代器的 Symbol.iterator);业务代码知道其存在即可。

8. 迭代器与生成器简介

任何对象实现了 [Symbol.iterator]() 就能被 for...of 遍历——数组、字符串、Map、Set、NodeList 都是如此,这就是为什么它们都能用 for…of 而 {} 不能。

生成器函数用 function*yield 定义惰性序列:

function* fibonacci() {
  let a = 0, b = 1;
  while (true) {
    yield a;
    [a, b] = [b, a + b];
  }
}
 
const fib = fibonacci();
console.log(fib.next().value); // 0
console.log(fib.next().value); // 1
console.log(fib.next().value); // 1
 
// 配合 for...of 消费(注意无限序列要 break)
for (const n of fibonacci()) {
  if (n > 100) break;
  console.log(n);
}

类比 Java:生成器约等于 Stream.iterate 或自定义 Iterator 实现,但语法侵入性小得多。生成器还可以委托(yield*)和双向通信(next 的参数会作为上一个 yield 的返回值),这些进阶用法在需要自定义数据流时再查文档即可。历史上 async/await 正是从生成器的暂停-恢复机制演化而来,理解”yield 即暂停点”这个概念对深入异步很有帮助。

日常业务少写生成器,掌握概念即可。


9. ES2016-ES2023 精选

这些”小而美”的特性在日常代码中出现频率极高,务必掌握。

7.1 可选链 ?. (ES2020)

安全地访问深层嵌套属性,中间环节 null/undefined 时短路返回 undefined 而不是抛错:

const resp = { data: { user: null } };
 
// 反例:层层手写判空
const city = resp && resp.data && resp.data.user ? resp.data.user.city : undefined;
 
// 可选链一行搞定
const city2 = resp?.data?.user?.city;    // undefined,不报错
resp.getData?.();                        // 方法也可能不存在时同样适用
const first = list?.[0];                 // 数组下标访问也适用

7.2 空值合并 ?? (ES2020)

只在左侧为 null 或 undefined 时取右侧——修正了 || 会误伤 0 和空串的问题:

const count = 0;
console.log(count || 10);  // 10 !0 被 || 当成假值,业务上往往错误
console.log(count ?? 10);  // 0 !只有 null/undefined 才走默认值
 
const input = "";
console.log(input ?? "匿名");  // "",保留了用户主动输入的空串

规则速记:处理”可能没提供”用 ??,处理”假值也算没意义”才用 ||

7.3 Array.at(ES2022)

负索引从尾部取元素,告别 arr[arr.length - 1]

const arr = [10, 20, 30, 40];
arr.at(-1);   // 40
arr.at(-2);   // 30
arr[99];      // undefined
arr.at(99);   // undefined,越界同样安全

7.4 structuredClone(ES2022 进浏览器)

原生的深拷贝函数(见 对象与数组 第 4 节),支持循环引用、Date、Map/Set,取代了 JSON 序列化 hack:

const original = { created: new Date(), meta: { tags: ["a", "b"] } };
original.self = original;              // 循环引用
const clone = structuredClone(original);
console.log(clone.meta.tags === original.meta.tags); // false,彻底隔离

7.5 其他高频补充

// Object.fromEntries:entries 的逆操作(ES2019)
const entries = [["a", 1], ["b", 2]];
Object.fromEntries(entries);   // { a: 1, b: 2 }
 
// flat / flatMap:扁平化数组(ES2019)
[1, [2, [3]]].flat(Infinity);              // [1, 2, 3]
["a,b", "c"].flatMap(s => s.split(","));   // ["a","b","c"]
 
// replaceAll(ES2021)
"a-b-c".replaceAll("-", "_");   // "a_b_c"
 
// 逻辑赋值运算符(ES2021)
obj.count ??= 0;    // 为 null/undefined 时赋默认值
obj.name ||= "未命名"; // 假值时兜底
obj.cache &&= {};   // 有值时才更新
 
// 数字分隔符(ES2021)
const budget = 1_000_000_000;
 
// Promise.withResolvers(ES2024):手动控制 Promise 的更顺手姿势
const { promise, resolve } = Promise.withResolvers();
setTimeout(() => resolve("ready"), 500);

10. 版本兼容与 Babel 一句话

现代浏览器的”常青版”(Chrome/Firefox/Safari/Edge 自动更新)已覆盖上述绝大部分特性;仅当需要支持老旧浏览器或极旧内核时,才需要 Babel 这类转译工具把新语法编译成等价旧语法,配合 polyfill 补齐缺失 API。工程化细节见后续构建工具章节,学习阶段直接使用最新 Chrome 即可。


11. 本章小结

  • ES6(2015) 是分水岭;之后按年发布,特性渐进可用。
  • 模块 import/export 取代全局变量共享;浏览器用 type="module"(自带 defer 与独立作用域);动态 import 支持按需加载。
  • Map/Set 补齐了标准容器:任意键的字典与去重集合。
  • # 私有字段让 class 拥有语言级封装,语义对齐 Java 的 private。
  • ?. 与 ?? 是防御性代码的两把瑞士军刀:前者防空指针链,后者精准区分”没有”与”假值”。
  • at/structuredClone/flatMap/逻辑赋值等小特性大幅提升代码简洁度。
  • 兼容性问题交给 Babel/polyfill,学习阶段无需关心。

下一步把这些语法全部用起来做一个完整应用:JS 实战:交互式页面