ES6+核心特性

14 分钟

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

维度SetArray
查找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

维度MapObject
键类型任意值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...inObject.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 有什么局限?

  1. 无法被 polyfill(Babel 不能完全转译)
  2. 对内置对象(如 Date、RegExp)的内部槽(internal slot)无法拦截
  3. this 指向问题 — 被代理对象内部方法中的 this 指向 Proxy 而非原始对象

可选链 ?. 与空值合并 ??

可选链操作符 ?.

安全地访问深层嵌套属性,遇到 nullundefined 时短路返回 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?.()(函数调用)。

空值合并操作符 ??

仅在左侧为 nullundefined 时取右侧值。与 || 的区别:|| 会把 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...offor...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.allall 在任一 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.racerace 返回第一个 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 更重要。