ES6+核心特性
Set 与 Map 数据结构
Set
Set 是值的集合,成员唯一且无序。与数组的核心区别在于自动去重,且判断相等使用的是 Same-value-zero 算法(类似 ===,但认为 NaN 等于 NaN)。
const set = new Set([1, 2, 2, 3, NaN, NaN]);
console.log(set); // Set(4) {1, 2, 3, NaN}
// 常用操作
set.add(4);
set.has(3); // true
set.delete(2);
set.size; // 3
// 数组去重
const unique = [...new Set(arr)];
Set vs Array:
| 维度 | Set | Array |
|---|---|---|
| 查找 | has() O(1) | includes() O(n) |
| 去重 | 天然去重 | 需额外处理 |
| 有序性 | 插入顺序 | 索引顺序 |
| 遍历 | forEach/for...of | 支持下标访问 |
Map
Map 是键值对集合,与 Object 的本质区别:键可以是任意类型。
const map = new Map();
const objKey = { id: 1 };
map.set(objKey, 'value bound to object');
map.set(1, 'number key');
map.set(true, 'boolean key');
map.get(objKey); // 'value bound to object'
map.size; // 3
Map vs Object:
| 维度 | Map | Object |
|---|---|---|
| 键类型 | 任意值 | String / Symbol |
| 有序性 | 严格插入序 | 整数键升序优先 |
| 大小 | .size 属性 | Object.keys().length |
| 迭代 | 直接可迭代 | 需 Object.keys() |
| 性能 | 频繁增删更优 | 静态结构更优 |
| 原型链 | 无原型污染 | 继承 Object.prototype |
WeakSet 与 WeakMap
「Weak」意味着对成员/键的引用是弱引用,不阻止垃圾回收。
let element = document.getElementById('app');
const weakSet = new WeakSet();
weakSet.add(element);
element = null; // element 被 GC 回收后,weakSet 中的引用自动消失
核心限制:
- WeakSet 只能存对象,WeakMap 的键只能是对象
- 不可枚举(无
size、无forEach、无keys()) - 无法被迭代
典型场景:DOM 节点标记、私有数据存储、防止内存泄漏的缓存。
🎯 面试追问:WeakRef 和 FinalizationRegistry 了解吗?
WeakRef(ES2021)提供对对象的弱引用,
deref()返回对象或undefined。FinalizationRegistry 可注册回调,在对象被 GC 时触发,用于释放外部资源。
解构赋值
数组解构
按位置对应提取值,支持默认值、跳位、rest 收集。
const [first, , third] = [1, 2, 3]; // 跳过第二个
const [a = 'default'] = [undefined]; // 默认值生效
const [head, ...tail] = [1, 2, 3, 4]; // head=1, tail=[2,3,4]
// 交换变量
let x = 1, y = 2;
[x, y] = [y, x];
对象解构
按属性名匹配,支持重命名、默认值、嵌套。
const { name: userName, age = 18 } = { name: 'Alice' };
// userName = 'Alice', age = 18
// 嵌套解构
const { data: { list = [], total } } = response;
// 函数参数解构
function render({ width = 100, height = 100, color = '#000' } = {}) {
// ...
}
注意事项
// 已声明变量的对象解构,需用括号包裹
let a;
({ a } = { a: 1 }); // 不加括号会被解析为块语句
// 解构 null/undefined 会报错
const { x } = null; // TypeError
🎯 面试追问:解构赋值的默认值何时生效?
只在对应位置的值严格等于
undefined时生效,null不会触发默认值。
Symbol
唯一性
Symbol 是 ES6 引入的第七种原始类型,每次调用 Symbol() 都产生全局唯一的值。
const s1 = Symbol('desc');
const s2 = Symbol('desc');
s1 === s2; // false — 描述相同,值不同
// 作为对象属性键,天然防冲突
const PRIVATE_ID = Symbol('id');
const obj = { [PRIVATE_ID]: 42, name: 'test' };
Object.keys(obj); // ['name'] — Symbol 键不会出现在常规遍历中
Object.getOwnPropertySymbols(obj); // [Symbol(id)]
Symbol.for 与全局注册表
Symbol.for(key) 在全局 Symbol 注册表中查找或创建,实现跨模块/跨 realm 共享同一 Symbol。
const s1 = Symbol.for('shared');
const s2 = Symbol.for('shared');
s1 === s2; // true
Symbol.keyFor(s1); // 'shared'
Symbol.keyFor(Symbol('local')); // undefined
内置 Symbol(Well-known Symbols)
// Symbol.iterator — 定义对象的默认迭代行为
const range = {
from: 1,
to: 5,
[Symbol.iterator]() {
let current = this.from;
return {
next: () => current <= this.to
? { value: current++, done: false }
: { done: true }
};
}
};
[...range]; // [1, 2, 3, 4, 5]
// Symbol.toPrimitive — 自定义类型转换
class Money {
constructor(amount, currency) {
this.amount = amount;
this.currency = currency;
}
[Symbol.toPrimitive](hint) {
if (hint === 'number') return this.amount;
if (hint === 'string') return `${this.amount} ${this.currency}`;
return this.amount;
}
}
🎯 面试追问:Symbol 属性是真正的"私有"吗?
不是。
Object.getOwnPropertySymbols()和Reflect.ownKeys()都可以获取 Symbol 键。只是不会出现在for...in、Object.keys()、JSON.stringify()中。真正的私有属性应使用 class 的#私有字段。
Proxy 与 Reflect
Proxy 基础
Proxy 可拦截对象的 13 种基本操作,构建「元编程」能力。
const handler = {
get(target, prop, receiver) {
console.log(`访问 ${String(prop)}`);
return Reflect.get(target, prop, receiver);
},
set(target, prop, value, receiver) {
if (typeof value !== 'number') {
throw new TypeError(`${String(prop)} must be a number`);
}
return Reflect.set(target, prop, value, receiver);
}
};
const data = new Proxy({ count: 0 }, handler);
data.count = 1; // 正常
data.count = 'a'; // TypeError: count must be a number
Reflect 的意义
Reflect 提供与 Proxy trap 一一对应的静态方法,统一了对象操作的调用方式:
- 将
Object上的命令式方法转为函数式调用 - 返回布尔值代替抛异常(如
Reflect.defineProperty返回true/false) - 配合 Proxy 正确转发操作,保证
this绑定正确(receiver参数)
Vue 3 响应式原理简述
Vue 3 的 reactive() 核心就是 Proxy:
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
track(target, key); // 收集依赖
const result = Reflect.get(target, key, receiver);
// 深层对象懒代理
return typeof result === 'object' && result !== null
? reactive(result)
: result;
},
set(target, key, value, receiver) {
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
if (oldValue !== value) {
trigger(target, key); // 触发更新
}
return result;
}
});
}
相比 Vue 2 的 Object.defineProperty:
- 可拦截属性新增/删除(无需
Vue.set) - 可拦截数组索引和
length变化 - 惰性代理嵌套对象,性能更优
- 支持 Map、Set 等集合类型
🎯 面试追问:Proxy 有什么局限?
- 无法被 polyfill(Babel 不能完全转译)
- 对内置对象(如 Date、RegExp)的内部槽(internal slot)无法拦截
this指向问题 — 被代理对象内部方法中的this指向 Proxy 而非原始对象
可选链 ?. 与空值合并 ??
可选链操作符 ?.
安全地访问深层嵌套属性,遇到 null 或 undefined 时短路返回 undefined。
const city = user?.address?.city;
const first = arr?.[0];
const result = obj?.method?.();
// 等价于
const city = user && user.address && user.address.city;
三种形式:obj?.prop(属性访问)、obj?.[expr](动态属性)、func?.()(函数调用)。
空值合并操作符 ??
仅在左侧为 null 或 undefined 时取右侧值。与 || 的区别:|| 会把 0、''、false 也视为假值。
const port = config.port ?? 3000;
// config.port = 0 → port = 0(保留有效的 0)
// config.port = undefined → port = 3000
// 对比 ||
const port2 = config.port || 3000;
// config.port = 0 → port2 = 3000(0 被覆盖,非预期)
逻辑赋值操作符(ES2021):
a ??= b; // a = a ?? b
a ||= b; // a = a || b
a &&= b; // a = a && b
🎯 面试追问:
?.和??能否与&&/||混用?
??不能直接与&&/||混用,需要加括号明确优先级,否则语法报错。这是 TC39 有意为之的设计,避免歧义。
模板字符串与标签模板
模板字符串
反引号包裹,支持多行文本和 ${} 插值表达式。
const greeting = `Hello, ${user.name}!
Your balance: ${formatMoney(user.balance)}`;
标签模板(Tagged Template)
模板字符串前加函数名,函数接收「字符串数组」和「插值结果」。
function highlight(strings, ...values) {
return strings.reduce((result, str, i) => {
const value = values[i] !== undefined ? `<mark>${values[i]}</mark>` : '';
return result + str + value;
}, '');
}
const name = 'ES6';
highlight`Feature: ${name} is great`;
// "Feature: <mark>ES6</mark> is great"
实际应用:
styled-components的 CSS-in-JS:styled.div`color: red;`- GraphQL 的
gql模板标签 - 国际化(i18n)转义处理
- SQL 防注入的参数化查询
for...of 与 Iterator 协议
Iterator 协议
任何对象实现了 [Symbol.iterator]() 方法返回一个迭代器(含 next() 方法),即为可迭代对象。
// 内置可迭代对象:Array, String, Map, Set, NodeList, arguments
// 自定义可迭代对象
class Fibonacci {
constructor(limit) {
this.limit = limit;
}
[Symbol.iterator]() {
let prev = 0, curr = 1, count = 0;
const limit = this.limit;
return {
next() {
if (count++ >= limit) return { done: true };
[prev, curr] = [curr, prev + curr];
return { value: prev, done: false };
}
};
}
}
[...new Fibonacci(8)]; // [1, 1, 2, 3, 5, 8, 13, 21]
for...of vs for...in
| 维度 | for...of | for...in |
|---|---|---|
| 遍历目标 | 可迭代对象的值 | 对象的可枚举键 |
| 原型链 | 不涉及 | 会遍历原型链 |
| 适用对象 | Array, Map, Set, String... | 普通对象 |
| 支持 break | ✅ | ✅ |
// for...of 配合解构
const userMap = new Map([['alice', 28], ['bob', 32]]);
for (const [name, age] of userMap) {
console.log(`${name}: ${age}`);
}
Generator 与迭代
Generator 函数天然实现 Iterator 协议,用 yield 逐步产出值:
function* idGenerator() {
let id = 0;
while (true) {
yield ++id;
}
}
const gen = idGenerator();
gen.next().value; // 1
gen.next().value; // 2
Promise 新增 API
Promise.allSettled(ES2020)
等待所有 Promise 完成(无论成功或失败),返回每个结果的状态描述。适用于「并发请求,互不依赖,需要知道每个结果」的场景。
const results = await Promise.allSettled([
fetch('/api/user'),
fetch('/api/orders'),
fetch('/api/recommendations')
]);
const successful = results
.filter(r => r.status === 'fulfilled')
.map(r => r.value);
const failed = results
.filter(r => r.status === 'rejected')
.map(r => r.reason);
vs Promise.all:all 在任一 reject 时立即短路,allSettled 永远等待全部完成。
Promise.any(ES2021)
返回第一个 fulfilled 的结果;全部 rejected 时抛出 AggregateError。
// 竞速多个镜像源,取最快成功的
const fastest = await Promise.any([
fetch('https://cdn1.example.com/data'),
fetch('https://cdn2.example.com/data'),
fetch('https://cdn3.example.com/data')
]);
vs Promise.race:race 返回第一个 settled(无论成功失败),any 只关心第一个成功。
四个静态方法对比
| 方法 | 短路条件 | 返回值 |
|---|---|---|
all | 任一 reject | 全部 fulfilled 的值数组 |
allSettled | 不短路 | 全部结果的状态描述数组 |
race | 任一 settled | 第一个 settled 的值/原因 |
any | 任一 fulfill | 第一个 fulfilled 的值 |
🎯 面试追问:如何实现一个带并发限制的 Promise 调度器?
核心思路:维护一个运行池(running)和等待队列(queue),每当运行池有空位就从队列取任务执行。完成时触发下一个任务入池。这是字节/美团的高频手写题。
总结
ES6+ 的特性不是孤立的语法糖,它们共同构建了现代 JavaScript 的编程范式:
- Set/Map 补齐了数据结构短板,配合 WeakRef 系列解决内存管理问题
- 解构赋值 让数据提取更声明式,减少中间变量
- Symbol 提供了元编程入口和属性隔离能力
- Proxy/Reflect 是框架级元编程的基石(Vue 3、MobX 5+)
- 可选链/空值合并 消灭了大量防御性编码
- Iterator 协议 统一了遍历接口,Generator 提供了惰性求值能力
- Promise 新 API 覆盖了并发控制的各种场景
面试中,这些特性往往不会单独考察,而是结合实际场景(如「用 Proxy 实现数据校验/响应式」「用 Symbol 实现私有属性」「Promise 并发控制」)综合出题。理解设计意图比记忆 API 更重要。