06 - ES6+ 特性
前置:异步编程。前几章已经零散用过不少新语法,本章系统盘点:哪些是 ES6 的里程碑特性,2016 年以后每年又补了什么好东西。
1. ES6 与年度发布节奏
ES6(官方名 ECMAScript 2015)是 JS 历史上最大的一次版本跃迁——let/const、class、模块、Promise、箭头函数等十余个核心特性同时落地,现代 JS 与”老 JS”的分界线就在这里。
此后规范改为年度发布:每年 6 月发布一版,特性按是否按时完成进入当年版本(ES2016、ES2017……ES2023 及以后)。这带来两个实际影响:
- 不再存在”等大版本”的憋大招,好特性按年渐进落地
- 学习时只需关注”哪一年加了什么”,浏览器支持情况查 caniuse 即可
- 阅读老教程/老代码时注意甄别:出现
var、require、回调风格的基本是 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 实战:交互式页面。