一、Set 数据结构
1. 基本概念
Set 是 ES6 引入的一种无序且唯一的值集合,类似于数组,但成员的值都是唯一的。
核心特性:
- 存储任何类型的唯一值(原始值或对象引用)
- 值的顺序即插入顺序
- 通过
===
判断值是否相等(NaN 例外,Set 中 NaN 等于自身)
javascript
const set = new Set();
set.add(1);
set.add('text');
set.add({name: 'John'});
console.log(set.size); // 3
2. 常用方法与属性
基本操作:
javascript
const set = new Set();
// 添加值
set.add(1).add(2).add(3); // 链式调用
// 检查存在
console.log(set.has(1)); // true
// 删除值
set.delete(1); // 返回布尔值表示是否删除成功
// 清空集合
set.clear();
// 获取大小
console.log(set.size); // 0
特殊值处理:
javascript
const set = new Set();
set.add(NaN);
console.log(set.has(NaN)); // true (与常规NaN !== NaN不同)
set.add(0);
set.add(-0);
console.log(set.size); // 2 (0和-0被视为不同值)
3. 遍历方法
javascript
const set = new Set(['red', 'green', 'blue']);
// forEach
set.forEach((value, valueAgain, set) => {
console.log(value); // red, green, blue
});
// for...of
for (const item of set) {
console.log(item);
}
// 获取迭代器
const iterator = set.values(); // 或 set[Symbol.iterator]()
console.log(iterator.next().value); // "red"
4. 实际应用场景
数组去重:
javascript
const numbers = [1, 2, 3, 3, 2, 1];
const unique = [...new Set(numbers)];
console.log(unique); // [1, 2, 3]
集合运算:
javascript
// 并集
const union = new Set([...setA, ...setB]);
// 交集
const intersection = new Set([...setA].filter(x => setB.has(x)));
// 差集
const difference = new Set([...setA].filter(x => !setB.has(x)));
二、Map 数据结构
1. 基本概念
Map 是键值对的集合,与 Object 类似,但有重要区别:
特性 | Map | Object |
---|---|---|
键类型 | 任意值(对象、函数等) | String 或 Symbol |
键顺序 | 插入顺序 | 无序(ES6后有一定顺序) |
大小获取 | size 属性 | 手动计算 |
性能 | 频繁增删键值对时更优 | 少量数据时可能更优 |
序列化 | 无原生支持 | 原生JSON支持 |
javascript
const map = new Map();
map.set('name', 'John');
map.set(1, 'number key');
map.set({}, 'object key');
console.log(map.size); // 3
2. 常用方法与属性
基本操作:
javascript
const map = new Map();
// 设置键值
map.set('key', 'value').set('key2', 'value2'); // 链式调用
// 获取值
console.log(map.get('key')); // "value"
// 检查存在
console.log(map.has('key')); // true
// 删除键
map.delete('key'); // 返回布尔值
// 清空Map
map.clear();
// 获取大小
console.log(map.size); // 0
特殊键处理:
javascript
const map = new Map();
const objKey = {};
map.set(objKey, 'value');
console.log(map.get(objKey)); // "value" (对象作为键)
map.set(NaN, 'not a number');
console.log(map.get(NaN)); // "not a number" (NaN作为键)
3. 遍历方法
javascript
const map = new Map([
['name', 'John'],
['age', 30]
]);
// forEach
map.forEach((value, key, map) => {
console.log(`${key}: ${value}`);
});
// for...of
for (const [key, value] of map) {
console.log(key, value);
}
// 获取迭代器
const keys = map.keys(); // 键迭代器
const values = map.values(); // 值迭代器
const entries = map.entries(); // 键值对迭代器
4. 初始化方式
javascript
// 通过二维数组初始化
const map1 = new Map([
['key1', 'value1'],
['key2', 'value2']
]);
// 从Object转换
const obj = { a: 1, b: 2 };
const map2 = new Map(Object.entries(obj));
// 从Map转回Object
const map3 = new Map([['a', 1], ['b', 2]]);
const obj2 = Object.fromEntries(map3);
console.log(obj2); // { a: 1, b: 2 }
三、Set 和 Map 的高级用法
1. 对象作为键的妙用
javascript
const user1 = { id: 1, name: 'John' };
const user2 = { id: 2, name: 'Jane' };
const userSettings = new Map();
userSettings.set(user1, { theme: 'dark' });
userSettings.set(user2, { theme: 'light' });
console.log(userSettings.get(user1).theme); // "dark"
2. 实现LRU缓存
javascript
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 oldestKey = this.cache.keys().next().value;
this.cache.delete(oldestKey);
}
this.cache.set(key, value);
}
}
3. 复杂数据结构
javascript
// 多层Map
const productLocations = new Map();
const warehouse1 = new Map();
warehouse1.set('A1', { product: 'Phone', quantity: 100 });
warehouse1.set('B2', { product: 'Laptop', quantity: 50 });
const warehouse2 = new Map();
warehouse2.set('C3', { product: 'Tablet', quantity: 200 });
productLocations.set('NY', warehouse1);
productLocations.set('CA', warehouse2);
console.log(productLocations.get('NY').get('A1').quantity); // 100
四、WeakSet 和 WeakMap
1. WeakSet
- 只能存储对象引用
- 不可遍历
- 弱引用(不计入垃圾回收引用计数)
javascript
const weakSet = new WeakSet();
let obj = {};
weakSet.add(obj);
console.log(weakSet.has(obj)); // true
obj = null; // 可被垃圾回收
2. WeakMap
- 键必须是对象
- 不可遍历
- 弱引用(键不计入垃圾回收引用计数)
javascript
const weakMap = new WeakMap();
let keyObj = { id: 1 };
weakMap.set(keyObj, 'private data');
console.log(weakMap.get(keyObj)); // "private data"
keyObj = null; // 键可被垃圾回收,关联值也会被清除
3. 使用场景
WeakMap 实现私有属性:
javascript
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"
// 外部无法访问privateData中的数据
WeakSet 标记对象:
javascript
const processedItems = new WeakSet();
function process(item) {
if (processedItems.has(item)) {
return;
}
// 处理逻辑...
processedItems.add(item);
}
五、性能比较与选择指南
1. Set vs Array
操作 | Set | Array |
---|---|---|
查找元素 | O(1) | O(n) |
添加元素 | O(1) | O(1) |
删除元素 | O(1) | O(n) |
去重 | 内置支持 | 需要额外处理 |
选择建议:
- 需要唯一值 → Set
- 需要索引/顺序操作 → Array
- 频繁查找/删除 → Set
2. Map vs Object
操作 | Map | Object |
---|---|---|
键类型 | 任意 | String/Symbol |
键顺序 | 插入顺序 | 特殊排序规则 |
大小获取 | size属性 | 手动计算 |
频繁增删 | 更优 | 较差 |
选择建议:
- 需要非字符串键 → Map
- 需要维护插入顺序 → Map
- 需要JSON序列化 → Object
- 少量固定属性 → Object
六、浏览器兼容性与Polyfill
1. 兼容性
- Set/Map:IE11+,所有现代浏览器
- WeakSet/WeakMap:IE11+,所有现代浏览器
2. Polyfill
对于旧版浏览器,可以使用以下polyfill:
html
<!-- 使用core-js的示例 -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/minified.js"></script>
<script>
// 现在可以使用Set/Map
const set = new Set([1, 2, 3]);
</script>
七、实际应用案例
1. 使用Set实现关键词高亮
javascript
function highlightKeywords(text, keywords) {
const keywordSet = new Set(keywords);
return text.split(' ').map(word =>
keywordSet.has(word) ? `<span class="highlight">${word}</span>` : word
).join(' ');
}
const result = highlightKeywords('Hello world this is a test', ['world', 'test']);
console.log(result);
// "Hello <span class="highlight">world</span> this is a <span class="highlight">test</span>"
2. 使用Map实现多语言支持
javascript
class I18n {
constructor() {
this.translations = new Map();
}
addLanguage(lang, data) {
this.translations.set(lang, data);
}
t(lang, key) {
const langData = this.translations.get(lang) || {};
return langData[key] || key;
}
}
const i18n = new I18n();
i18n.addLanguage('en', { welcome: 'Welcome' });
i18n.addLanguage('es', { welcome: 'Bienvenido' });
console.log(i18n.t('en', 'welcome')); // "Welcome"
console.log(i18n.t('es', 'welcome')); // "Bienvenido"
3. 使用WeakMap实现DOM节点元数据
javascript
const nodeMetadata = new WeakMap();
function setNodeData(node, data) {
nodeMetadata.set(node, data);
}
function getNodeData(node) {
return nodeMetadata.get(node);
}
const div = document.createElement('div');
setNodeData(div, { clicked: 0 });
div.addEventListener('click', () => {
const data = getNodeData(div);
data.clicked++;
console.log(`Clicked ${data.clicked} times`);
});
八、总结与最佳实践
1. Set/Map 优势总结
- 更直观的API :专用方法如
has()
,delete()
等 - 更好的性能:大规模数据操作时表现更优
- 更丰富的键类型:支持对象等复杂类型作为键
- 维护插入顺序:遍历时按插入顺序返回元素
2. 使用建议
- 优先考虑语义:当需要集合语义时使用Set,需要键值对时使用Map
- 注意内存管理:大型数据集考虑使用WeakMap/WeakSet避免内存泄漏
- 合理初始化:已知初始值时使用构造参数而非多次调用add/set
- 利用解构:与扩展运算符结合转换Set/Map为数组
- 类型检查 :使用
instanceof Set/Map
进行类型判断
3. 性能优化技巧
- 避免频繁的Set/Map创建和销毁
- 对于大型集合,预先分配足够容量(某些JavaScript引擎优化)
- 优先使用原生方法而非转换为数组操作
- WeakMap/WeakSet适合临时关联数据场景