【秋招备战】JavaScript 集合类型深度解析

【秋招备战】JavaScript 集合类型深度解析

Simon Lv1

在 JavaScript 的世界里,除了我们熟悉的对象(Object)和数组(Array),ES6 还引入了四种新的集合类型:Map、Set、WeakMap 和 WeakSet。这些数据结构不仅丰富了我们的工具箱,更是在现代前端框架的底层实现中扮演着重要角色。今天,让我们深入探讨这些集合类型的特性、使用场景,以及它们在 React 和 Vue 源码中的精妙应用。

一、Map:更强大的键值对集合

基本概念

Map 是一个键值对的集合,与普通对象最大的区别在于:Map 的键可以是任意类型,而不仅仅是字符串或 Symbol。

const map = new Map();

// 基本用法
map.set('name', 'JavaScript');
map.set(42, 'The Answer');
map.set(true, 'Boolean key');

// 对象作为键
const objKey = { id: 1 };
map.set(objKey, 'Object as key');

// 函数作为键
const funcKey = () => {};
map.set(funcKey, 'Function as key');

console.log(map.get(objKey)); // 'Object as key'
console.log(map.size); // 5

Map 的核心特性

  1. 键的唯一性:Map 中的键是唯一的,重复设置会覆盖之前的值
  2. 保持插入顺序:Map 会记住键值对的插入顺序
  3. 可迭代:Map 实现了迭代器协议
const map = new Map([
['first', 1],
['second', 2],
['third', 3]
]);

// 遍历方法
for (const [key, value] of map) {
console.log(`${key}: ${value}`);
}

// 获取所有键
console.log([...map.keys()]); // ['first', 'second', 'third']

// 获取所有值
console.log([...map.values()]); // [1, 2, 3]

// 获取所有键值对
console.log([...map.entries()]); // [['first', 1], ['second', 2], ['third', 3]]

Map vs Object 的性能对比

// 性能测试:频繁增删操作
const testSize = 100000;

// Object 测试
console.time('Object');
const obj = {};
for (let i = 0; i < testSize; i++) {
obj[`key${i}`] = i;
delete obj[`key${i}`];
}
console.timeEnd('Object');

// Map 测试
console.time('Map');
const map = new Map();
for (let i = 0; i < testSize; i++) {
map.set(`key${i}`, i);
map.delete(`key${i}`);
}
console.timeEnd('Map');


// Object: 40.439ms
// Map: 15.457ms
// Map 在频繁增删操作时性能更优

实际应用场景

// 缓存管理
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.cache = new Map();
}

get(key) {
if (!this.cache.has(key)) return -1;

// 将访问的元素移到最后(最新)
const value = this.cache.get(key);
this.cache.delete(key);
this.cache.set(key, value);
return value;
}

put(key, value) {
if (this.cache.has(key)) {
this.cache.delete(key);
}

this.cache.set(key, value);

// 超出容量,删除最旧的(第一个)
if (this.cache.size > this.capacity) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
}
}

二、Set:独特值的集合

基本概念

Set 是一个值的集合,其中每个值都是唯一的,不会重复。

const set = new Set();

// 添加值
set.add(1);
set.add(2);
set.add(2); // 重复值不会被添加
set.add('2'); // 字符串'2'和数字2是不同的

console.log(set.size); // 3
console.log(set.has(2)); // true

// 数组去重
const numbers = [1, 2, 2, 3, 4, 4, 5];
const uniqueNumbers = [...new Set(numbers)];
console.log(uniqueNumbers); // [1, 2, 3, 4, 5]

Set 的高级应用

// 1. 数组操作
const setA = new Set([1, 2, 3, 4]);
const setB = new Set([3, 4, 5, 6]);

// 并集
const union = new Set([...setA, ...setB]);
console.log([...union]); // [1, 2, 3, 4, 5, 6]

// 交集
const intersection = new Set([...setA].filter(x => setB.has(x)));
console.log([...intersection]); // [3, 4]

// 差集
const difference = new Set([...setA].filter(x => !setB.has(x)));
console.log([...difference]); // [1, 2]

// 2. 对象去重
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 1, name: 'Alice' },
{ id: 3, name: 'Charlie' }
];

const uniqueUsers = Array.from(
new Map(users.map(user => [user.id, user])).values()
);
console.log(uniqueUsers); // 去重后的用户数组

性能优势

// Set 的 has 方法性能测试
const arr = Array.from({ length: 10000 }, (_, i) => i);
const set = new Set(arr);

console.time('Array includes');
for (let i = 0; i < 1000; i++) {
arr.includes(9999);
}
console.timeEnd('Array includes');

console.time('Set has');
for (let i = 0; i < 1000; i++) {
set.has(9999);
}
console.timeEnd('Set has');

// Set.has() 的时间复杂度是 O(1),而 Array.includes() 是 O(n)

三、WeakMap:弱引用的键值对集合

核心特性

WeakMap 与 Map 类似,但有几个关键区别:

  1. 键必须是对象(不能是原始值)
  2. 键是弱引用的,不会阻止垃圾回收
  3. 不可迭代,没有 size 属性
const wm = new WeakMap();

// 只能使用对象作为键
const obj1 = { name: 'object1' };
const obj2 = { name: 'object2' };

wm.set(obj1, 'value1');
wm.set(obj2, 'value2');

console.log(wm.get(obj1)); // 'value1'

// 不能使用原始值作为键
// wm.set('string', 'value'); // TypeError

// 弱引用特性
let obj3 = { name: 'object3' };
wm.set(obj3, 'value3');
obj3 = null; // obj3 可以被垃圾回收,对应的 WeakMap 条目也会被清除

WeakMap 的实际应用

// 1. 私有属性实现
const privateData = new WeakMap();

class Person {
constructor(name, age) {
privateData.set(this, { name, age });
}

getName() {
return privateData.get(this).name;
}

getAge() {
return privateData.get(this).age;
}
}

const person = new Person('Alice', 30);
console.log(person.getName()); // 'Alice'
console.log(person.name); // undefined

// 2. DOM 元素关联数据
const elementData = new WeakMap();

function attachData(element, data) {
elementData.set(element, data);
}

function getData(element) {
return elementData.get(element);
}

// 使用
const button = document.querySelector('#myButton');
attachData(button, { clickCount: 0 });

button.addEventListener('click', () => {
const data = getData(button);
data.clickCount++;
console.log(`Clicked ${data.clickCount} times`);
});

内存管理优势

// 使用 Map 可能造成内存泄漏
const cache = new Map();

function processData(obj) {
if (cache.has(obj)) {
return cache.get(obj);
}

const result = expensiveOperation(obj);
cache.set(obj, result); // obj 被 Map 引用,无法被垃圾回收
return result;
}

// 使用 WeakMap 避免内存泄漏
const weakCache = new WeakMap();

function processDataSafe(obj) {
if (weakCache.has(obj)) {
return weakCache.get(obj);
}

const result = expensiveOperation(obj);
weakCache.set(obj, result); // obj 可以被正常垃圾回收
return result;
}

四、WeakSet:弱引用的值集合

基本特性

WeakSet 与 Set 类似,但具有以下特点:

  1. 只能存储对象
  2. 对象是弱引用的
  3. 不可迭代
const ws = new WeakSet();

const obj1 = { id: 1 };
const obj2 = { id: 2 };

ws.add(obj1);
ws.add(obj2);

console.log(ws.has(obj1)); // true

// 不能添加原始值
// ws.add(1); // TypeError

// 防止重复添加
ws.add(obj1); // 不会报错,但也不会重复添加

实际应用场景

// 1. 标记对象状态
const disabledElements = new WeakSet();

function disableElement(element) {
disabledElements.add(element);
element.classList.add('disabled');
}

function enableElement(element) {
disabledElements.delete(element);
element.classList.remove('disabled');
}

function isDisabled(element) {
return disabledElements.has(element);
}

// 2. 防止递归调用
const processing = new WeakSet();

function processObject(obj) {
if (processing.has(obj)) {
console.log('Already processing this object');
return;
}

processing.add(obj);

try {
// 处理对象
console.log('Processing:', obj);

// 如果对象有子对象,递归处理
if (obj.children) {
obj.children.forEach(child => processObject(child));
}
} finally {
processing.delete(obj);
}
}

五、在 React 中的应用

React Fiber 中的 WeakMap 使用

React 的 Fiber 架构中使用 WeakMap 来存储组件相关的信息:

// React 源码简化示例
const fiberNodeMap = new WeakMap();

function createFiberNode(element) {
const fiber = {
type: element.type,
props: element.props,
// ... 其他 fiber 属性
};

// 将 DOM 元素与 Fiber 节点关联
if (element.dom) {
fiberNodeMap.set(element.dom, fiber);
}

return fiber;
}

function getFiberFromDOM(domElement) {
return fiberNodeMap.get(domElement);
}

React Hooks 中的 Map 应用

// React Hooks 实现原理简化
let currentComponent = null;
const hooksMap = new Map();

function useState(initialState) {
const component = currentComponent;

if (!hooksMap.has(component)) {
hooksMap.set(component, []);
}

const hooks = hooksMap.get(component);
const hookIndex = hooks.length;

if (hooks[hookIndex] === undefined) {
hooks[hookIndex] = {
state: initialState,
setState: (newState) => {
hooks[hookIndex].state = newState;
reRender(component);
}
};
}

return [hooks[hookIndex].state, hooks[hookIndex].setState];
}

六、在 Vue 中的应用

Vue 3 响应式系统中的 WeakMap

Vue 3 的响应式系统大量使用了 WeakMap 来存储依赖关系:

// Vue 3 响应式原理简化
const targetMap = new WeakMap();

function track(target, key) {
let depsMap = targetMap.get(target);
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()));
}

let dep = depsMap.get(key);
if (!dep) {
depsMap.set(key, (dep = new Set()));
}

dep.add(activeEffect);
}

function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;

const dep = depsMap.get(key);
if (!dep) return;

dep.forEach(effect => effect());
}

// 创建响应式对象
function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
track(target, key);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
const result = Reflect.set(target, key, value, receiver);
trigger(target, key);
return result;
}
});
}

Vue 组件实例管理

// Vue 组件实例缓存
const instanceMap = new WeakMap();

class VueComponent {
constructor(options) {
this.options = options;

// 将组件实例与其 DOM 元素关联
if (options.el) {
instanceMap.set(options.el, this);
}
}

static getInstance(element) {
return instanceMap.get(element);
}
}

// KeepAlive 组件的缓存实现
const cache = new Map();
const keys = new Set();

function pruneCache(keepAliveInstance, filter) {
cache.forEach((entry, key) => {
const name = getComponentName(entry.componentInstance);
if (name && !filter(name)) {
pruneCacheEntry(key);
}
});
}

function pruneCacheEntry(key) {
const cached = cache.get(key);
if (cached) {
cached.componentInstance.$destroy();
}
cache.delete(key);
keys.delete(key);
}

七、性能优化最佳实践

选择合适的数据结构

// 场景1:需要频繁检查元素是否存在
// ❌ 不推荐:使用数组
const permissions = ['read', 'write', 'delete'];
if (permissions.includes('write')) { } // O(n)

// ✅ 推荐:使用 Set
const permissionSet = new Set(['read', 'write', 'delete']);
if (permissionSet.has('write')) { } // O(1)

// 场景2:需要关联 DOM 元素和数据
// ❌ 不推荐:使用 Map(可能造成内存泄漏)
const elementDataMap = new Map();

// ✅ 推荐:使用 WeakMap
const elementDataWeakMap = new WeakMap();

// 场景3:需要有序的键值对
// ✅ Map 保持插入顺序
const orderedMap = new Map([
['first', 1],
['second', 2],
['third', 3]
]);

内存管理注意事项

// 避免内存泄漏的模式
class EventManager {
constructor() {
// 使用 WeakMap 存储事件监听器
this.listeners = new WeakMap();
}

addEventListener(element, event, handler) {
if (!this.listeners.has(element)) {
this.listeners.set(element, new Map());
}

const elementListeners = this.listeners.get(element);
if (!elementListeners.has(event)) {
elementListeners.set(event, new Set());
}

elementListeners.get(event).add(handler);
element.addEventListener(event, handler);
}

removeEventListener(element, event, handler) {
const elementListeners = this.listeners.get(element);
if (!elementListeners) return;

const eventHandlers = elementListeners.get(event);
if (!eventHandlers) return;

eventHandlers.delete(handler);
element.removeEventListener(event, handler);

// 清理空的集合
if (eventHandlers.size === 0) {
elementListeners.delete(event);
}
if (elementListeners.size === 0) {
this.listeners.delete(element);
}
}
}

八、总结与建议

何时使用 Map/Set

  • Map:当你需要键值对集合,且键可能是对象或需要保持插入顺序时
  • Set:当你需要存储唯一值,或进行集合运算(并集、交集、差集)时

何时使用 WeakMap/WeakSet

  • WeakMap:当你需要将数据与对象关联,且不想阻止对象被垃圾回收时
  • WeakSet:当你需要标记对象,或创建对象的弱引用集合时

性能考虑

  1. 查找性能:Map/Set 的查找是 O(1),而数组是 O(n)
  2. 内存管理:WeakMap/WeakSet 有助于防止内存泄漏
  3. 迭代性能:Map/Set 可以直接迭代,性能优于对象的 Object.keys()

这四种集合类型极大地丰富了 JavaScript 的数据结构选择,合理使用它们不仅能让代码更简洁、更具表现力,还能在性能和内存管理方面带来显著的提升。在现代前端框架的开发中,它们更是不可或缺的基础设施。

  • 标题: 【秋招备战】JavaScript 集合类型深度解析
  • 作者: Simon
  • 创建于 : 2025-08-05 18:03:46
  • 更新于 : 2025-08-05 18:07:33
  • 链接: https://www.simonicle.cn/2025/08/05/【秋招备战】JavaScript 集合类型深度解析/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论