icon
集合类型

集合类型

Set 和 Map 是 ES6 引入的新的数据结构,用于存储唯一值和键值对。

Set

Set 是值的集合,每个值只能出现一次。

  1. 基本用法
const set = new Set();
set.add(1);
set.add(2);
set.add(2); // 重复值,不会添加
console.log(set.size); // 2

// 使用数组初始化
const set2 = new Set([1, 2, 3, 3, 4]);
console.log([...set2]); // [1, 2, 3, 4]
  1. 常用方法
const set = new Set([1, 2, 3]);

// 添加元素
set.add(4);
console.log(set.size); // 4

// 删除元素
set.delete(2);
console.log(set.has(2)); // false

// 检查是否存在
console.log(set.has(1)); // true

// 清空
set.clear();
console.log(set.size); // 0
  1. 遍历
const set = new Set([1, 2, 3]);

// for...of
for (const value of set) {
  console.log(value);
}

// forEach
set.forEach(value => {
  console.log(value);
});

// 转换为数组
const arr = [...set];
const arr2 = Array.from(set);

Set 实际应用

  1. 数组去重
const arr = [1, 2, 2, 3, 3, 3, 4];
const unique = [...new Set(arr)];
console.log(unique); // [1, 2, 3, 4]
  1. 字符串去重
const str = 'hello';
const unique = [...new Set(str)].join('');
console.log(unique); // 'helo'
  1. 集合运算
// 并集
function union(setA, setB) {
  return new Set([...setA, ...setB]);
}

// 交集
function intersection(setA, setB) {
  return new Set([...setA].filter(x => setB.has(x)));
}

// 差集
function difference(setA, setB) {
  return new Set([...setA].filter(x => !setB.has(x)));
}

const set1 = new Set([1, 2, 3]);
const set2 = new Set([2, 3, 4]);

console.log([...union(set1, set2)]);         // [1, 2, 3, 4]
console.log([...intersection(set1, set2)]); // [2, 3]
console.log([...difference(set1, set2)]);   // [1]
  1. 跟踪已访问的元素
function processItems(items) {
  const processed = new Set();
  
  for (const item of items) {
    if (!processed.has(item)) {
      process(item);
      processed.add(item);
    }
  }
}

WeakSet

WeakSet 是 Set 的弱引用版本,只能存储对象。

const weakSet = new WeakSet();
const obj1 = {};
const obj2 = {};

weakSet.add(obj1);
weakSet.add(obj2);

console.log(weakSet.has(obj1)); // true
weakSet.delete(obj1);
console.log(weakSet.has(obj1)); // false

// 注意:WeakSet 不可迭代,没有 size 属性

特点:

  • 只能存储对象
  • 弱引用,不影响垃圾回收
  • 不可迭代
  • 没有 size 属性

Map

Map 是键值对的集合,任何值都可以作为键。

  1. 基本用法
const map = new Map();
map.set('name', 'Alice');
map.set('age', 25);

console.log(map.get('name')); // 'Alice'
console.log(map.size);        // 2

// 使用数组初始化
const map2 = new Map([
  ['name', 'Alice'],
  ['age', 25]
]);
  1. 常用方法
const map = new Map();

// 设置键值对
map.set('key1', 'value1');
map.set('key2', 'value2');

// 获取值
console.log(map.get('key1')); // 'value1'

// 检查键是否存在
console.log(map.has('key1')); // true

// 删除键值对
map.delete('key1');

// 清空
map.clear();

// 获取大小
console.log(map.size);
  1. 遍历
const map = new Map([
  ['name', 'Alice'],
  ['age', 25]
]);

// 遍历键值对
for (const [key, value] of map) {
  console.log(key, value);
}

// forEach
map.forEach((value, key) => {
  console.log(key, value);
});

// 获取所有键
for (const key of map.keys()) {
  console.log(key);
}

// 获取所有值
for (const value of map.values()) {
  console.log(value);
}

// 获取所有条目
for (const entry of map.entries()) {
  console.log(entry);
}

Map vs Object

| 特性 | Map | Object | |------|-----|--------| | 键类型 | 任何值 | 字符串或 Symbol | | 大小 | size 属性 | 手动计算 | | 迭代 | 可迭代 | 需要 Object.keys() | | 性能 | 频繁增删更好 | 频繁增删较差 | | 默认键 | 无 | 有原型链 |

// Map 可以使用任何类型作为键
const map = new Map();
map.set(1, 'number');
map.set(true, 'boolean');
map.set({}, 'object');
map.set(function() {}, 'function');

// Object 的键会被转换为字符串
const obj = {};
obj[1] = 'number';
obj[true] = 'boolean';
console.log(obj); // { '1': 'number', 'true': 'boolean' }

Map 实际应用

  1. 缓存
const cache = new Map();

function expensiveOperation(key) {
  if (cache.has(key)) {
    return cache.get(key);
  }
  
  const result = computeExpensiveValue(key);
  cache.set(key, result);
  return result;
}
  1. 计数
function countOccurrences(arr) {
  const counts = new Map();
  
  for (const item of arr) {
    counts.set(item, (counts.get(item) || 0) + 1);
  }
  
  return counts;
}

const arr = ['a', 'b', 'a', 'c', 'b', 'a'];
const counts = countOccurrences(arr);
console.log(counts.get('a')); // 3
  1. 对象关联
const userMetadata = new Map();

const user1 = { id: 1, name: 'Alice' };
const user2 = { id: 2, name: 'Bob' };

userMetadata.set(user1, { lastLogin: Date.now(), role: 'admin' });
userMetadata.set(user2, { lastLogin: Date.now(), role: 'user' });

console.log(userMetadata.get(user1)); // { lastLogin: ..., role: 'admin' }
  1. DOM 元素关联数据
const elementData = new Map();

const button = document.querySelector('button');
elementData.set(button, { clicks: 0, lastClick: null });

button.addEventListener('click', () => {
  const data = elementData.get(button);
  data.clicks++;
  data.lastClick = Date.now();
  elementData.set(button, data);
});
  1. 实现 LRU 缓存
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);
    } else if (this.cache.size >= this.capacity) {
      // 删除最久未使用的(第一个)
      const firstKey = this.cache.keys().next().value;
      this.cache.delete(firstKey);
    }
    
    this.cache.set(key, value);
  }
}

WeakMap

WeakMap 是 Map 的弱引用版本,键必须是对象。

const weakMap = new WeakMap();
const obj1 = {};
const obj2 = {};

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

console.log(weakMap.get(obj1)); // 'value1'
weakMap.delete(obj1);
console.log(weakMap.has(obj1)); // false

// 注意:WeakMap 不可迭代,没有 size 属性

特点:

  • 键必须是对象
  • 弱引用,不影响垃圾回收
  • 不可迭代
  • 没有 size 属性

应用场景:

// 私有数据存储
const privateData = new WeakMap();

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

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

const person = new Person('Alice');
console.log(person.getName()); // 'Alice'
// person.name 不存在,实现了私有属性

注意事项

  1. Set 和 Map 使用 SameValueZero 比较
const set = new Set();
set.add(NaN);
set.add(NaN);
console.log(set.size); // 1 (NaN === NaN 为 false,但 Set 认为相等)

set.add(0);
set.add(-0);
console.log(set.size); // 2 (0 === -0 为 true,但 Set 认为相等)
  1. 对象作为键
const map = new Map();
const obj1 = { id: 1 };
const obj2 = { id: 1 };

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

console.log(map.get(obj1)); // 'value1'
console.log(map.get(obj2)); // 'value2'
// 不同的对象引用,即使内容相同也是不同的键
  1. 性能考虑
// Map 在频繁增删时性能更好
const map = new Map();
for (let i = 0; i < 1000000; i++) {
  map.set(i, i);
}

// Object 在频繁增删时性能较差
const obj = {};
for (let i = 0; i < 1000000; i++) {
  obj[i] = i;
}
  1. 序列化
const map = new Map([['name', 'Alice']]);
console.log(JSON.stringify(map)); // '{}' (Map 不能直接序列化)

// 需要转换为数组
const arr = [...map];
console.log(JSON.stringify(arr)); // '[["name","Alice"]]'

最佳实践

  1. 需要唯一值时使用 Set
const uniqueIds = new Set([1, 2, 2, 3, 3]);
  1. 需要键值对且键不是字符串时使用 Map
const userMap = new Map();
userMap.set(userObject, userData);
  1. 需要弱引用时使用 WeakSet/WeakMap
const elementData = new WeakMap();
elementData.set(domElement, data);
  1. 需要频繁增删时使用 Map 而不是 Object
const cache = new Map(); // 而不是 {}