6.4 Map
Map 是 ES6 引入的一种新的集合类型,为 JavaScript 带来了真正的键值对存储机制。
6.4.1 基本API
Map可以支持使用任何JavaScript数据类型作为键,而Object只能使用数值、字符串和符号作为键。
javascript
//使用new创建一个空映射
const m = new Map();
//使用嵌套数组初始化映射
const m1 = new Map([
["key1", "val1"],
["key2", "val2"],
["key3", "val3"],
["key4", "val4"],
])
// 初始化后的基本操作
alert(m1.size); //获取键值对数量:4
m.set("name", "John"); // 设置或添加键值对
m.set({ id: 1 }, "object key"); // 对象作为键
console.log(m.get("name")); // "John"
console.log(m.has("name")); // true
m.delete("name"); // 删除
m.clear(); // 清空
6.4.2 顺序与迭代
与Object类型的一个主要差异是,Map实例会维护键值对的插入顺序,因此可以根据顺序进行迭代操作。
javascript
const m = new Map([
["key1", "val1"],
["key2", "val2"],
["key3", "val3"]
])
alert(m.entries === m[Symbol.iterator]); //true
// 遍历方法
m.forEach((value, key) => {
console.log(`${key}: ${value}`);
});
for (let pair of m[Symbol.iterator]()){
alert(pair);
}
//[key1,val1]
//[key2,val2]
//[key3,val3]
//entries()获取映射实例的迭代器,返回[key, value]形式的数组。
for (let [key, value] of m.entries()) {
console.log(key, value);
}
//keys()返回以插入顺序生成键的迭代器
for (let key of m.keys()) {
console.log(key);
}
//values()返回以插入顺序生成值的迭代器
for (let value of m.values()) {
console.log(value);
}
键和值在迭代器遍历时是可以修改的,但作为键的字符串原始值是不能修改的。如果修改了作为键的对象的属性值,但是对象在映射内部依然引用相同的值,无影响。
javascript
const m = new Map([
["key1", "value1"]
]);
for(let key of m.keys()){
key = "newKey"
console.log(key) //newKey
console.log(m.get("key1")) //value1
}
const keyObj = {id:1};
const m2 = new Map([
[keyObj, "value1"]
]);
for(let key of m2.keys()){
key.id = "newKey"
console.log(key) //{id: 'newKey'}
console.log(m2.get(keyObj)) //value1
}
console.log(keyObj); //{id: 'newKey'}
6.4.3 与Object的对比
|------|---------|-------------------|
| 特性 | Map | Object |
| 键的类型 | 任何类型 | String 或 Symbol |
| 键的顺序 | 插入顺序 | 不确定 |
| 大小获取 | size 属性 | 手动计算 |
| 性能 | 频繁增删时更优 | 未优化 |
| 序列化 | 无原生支持 | JSON.stringify 支持 |
6.5 WeakMap
ECMAScript6新增WeakMap"弱映射"是一种新的集合类型。WeakMap 是 Map 的"弱"版本,主要区别在于键必须是Object,且是弱引用,其API也是Map的子集。
6.5.1 基本API
javascript
//使用new实例化一个空的WeakMap对象
const wm = new WeakMap();
const key1 = {id:1};
const key2 = {id:2};
const key3 = {id:3};
//使用嵌套数组初始化WeakMap对象
const wm1 = new WeakMap([
[key1, 'key1'],
[key2, 'key2'],
[key3, 'key3']
]);
//初始化完成后,可以通过多种方法进行操作
console.log(wm1.get(key1)); //使用get()方法获取key1的值
console.log(wm1.has(key1)); //使用has()方法判断key1是否存在
wm1.set(key1, 'key1-new');//使用set()方法设置key1的值
wm1.delete(key1);//使用delete()方法删除key1
6.5.2 弱键
WeakMap中的键不属于正式的引用,不会阻止垃圾回收。
javascript
let obj = { data: "important" };
const wm = new WeakMap();
wm.set(obj, "metadata");
// 当 obj 不再被引用时,垃圾回收器可以回收这个对象
obj = null; // 此时 { data: "important" } 可以被垃圾回收
//然后弱映射中的该键值对也会被清理掉。
6.5.3 不可迭代键
因为WeakMap中的键/值对任何时候都可能被销毁,所以没有必要提供迭代能力。也就导致了不可能在不知道对象引用情况下从弱映射中获取值。保证了只有通过键对象的引用才能取得值。
6.5.4 现代应用场景
私有属性实现
私有变量会存储在弱映射中,以对象实例为键,以私有成员的字典为值。
javascript
const privateData = new WeakMap();
class User {
constructor(name, email) {
// 存储私有数据
privateData.set(this, {
name: name,
email: email,
internalId: Symbol('id')
});
}
getName() {
return privateData.get(this).name;
}
getEmail() {
return privateData.get(this).email;
}
// 公共属性
get id() {
return privateData.get(this).internalId.toString();
}
}
const user = new User("Alice", "alice@example.com");
console.log(user.getName()); // "Alice"
// 无法直接访问 privateData 中的私有属性
DOM 元素元数据
因为WeakMap不会妨碍垃圾回收,因此非常适合保持关联元数据。
javascript
const elementMetadata = new WeakMap();
function enhanceElement(element, metadata) {
elementMetadata.set(element, {
created: new Date(),
...metadata
});
// 当 DOM 元素被移除时,对应的元数据会自动被垃圾回收
}
function getElementInfo(element) {
return elementMetadata.get(element);
}
// 使用示例
const div = document.createElement('div');
enhanceElement(div, { type: 'custom', version: '1.0' });
console.log(getElementInfo(div)); // 显示元数据
6.5.5 现代技术中的最佳实践
React/Vue 中的使用
javascript
// React 自定义 Hook 中使用 WeakMap
function usePrivateStore() {
const store = React.useRef(new WeakMap());
const setPrivate = React.useCallback((key, value) => {
store.current.set(key, value);
}, []);
const getPrivate = React.useCallback((key) => {
return store.current.get(key);
}, []);
return { setPrivate, getPrivate };
}
// Vue 3 组合式 API
export function useComponentState() {
const states = new WeakMap();
const setState = (component, state) => {
states.set(component, reactive(state));
};
const getState = (component) => {
return states.get(component);
};
return { setState, getState };
}
性能优化模式
javascript
// 使用 Map 进行函数记忆化
function memoize(fn) {
const cache = new Map();
return function(...args) {
const key = JSON.stringify(args);
if (cache.has(key)) {
return cache.get(key);
}
const result = fn.apply(this, args);
cache.set(key, result);
return result;
};
}
// 使用 WeakMap 进行对象缓存
const renderCache = new WeakMap();
function expensiveRender(element) {
if (renderCache.has(element)) {
return renderCache.get(element);
}
const result = performExpensiveCalculation(element);
renderCache.set(element, result);
return result;
}
TypeScript 集成
TypeScript
// Map 的 TypeScript 类型
interface User {
id: number;
name: string;
}
const userMap: Map<number, User> = new Map();
userMap.set(1, { id: 1, name: "John" });
// WeakMap 的 TypeScript 类型
class Component {
constructor(public id: string) {}
}
const componentData: WeakMap<Component, { created: Date }> = new WeakMap();
const comp = new Component("btn1");
componentData.set(comp, { created: new Date() });
6.5.6 与 Map 的关键区别
|------|-----------|-----------|
| 特性 | WeakMap | Map |
| 键类型 | 只能是对象 | 任意类型 |
| 可迭代 | 否 | 是 |
| 可清空 | 否 | clear() |
| 大小 | 无 size 属性 | 有 size 属性 |
| 垃圾回收 | 键是弱引用 | 键是强引用 |
实际项目中的使用建议
何时使用 Map
- 需要维护键值对的插入顺序时
- 需要频繁添加和删除键值对时
- 键的类型多样(非字符串)时
- 需要知道集合大小时
何时使用 WeakMap
- 需要为对象存储私有数据时
- 管理 DOM 元素的元数据时
- 避免内存泄漏是关键考虑时
- 不需要遍历或知道大小时
6.6 Set
ECMAScript6新增的Set是一种新的集合类型,很多方面像加强版的Map,且大多数API和行为是共有的。它类似于数组,但成员的值都是唯一的。
6.6.1 基本API和迭代方法
与Map类似,Set可以包含任何JavaScript数据类型作为值。Set会维护值插入时的顺序,因此支持按顺序迭代。
javascript
//使用new关键字创建一个Set对象
const s = new Set();
//使用数组初始化集合
const s1 = new Set(["val1", "val2", "val3"]);
// 基本操作
s.add(1); // 添加值
s.add(2);
s.add(2); // 重复添加无效
console.log(s.has(1)); // true
console.log(s.size); // 2
s.delete(1); // 删除值, 返回是否删除成功的布尔值
s.clear(); // 清空集合
// 迭代与遍历方法
s.forEach(value => {
console.log(value);
});
for (let value of s.values()) {
console.log(value);
}
for (let value of s) {
console.log(value);
}
for (let pair of s.entries()) {
console.log(pair);
}
for (let pair of s[Symbol.iterator]()) {
console.log(pair);
}
6.6.2 现代应用场景
数组去重
javascript
// 传统方式
function uniqueArray(arr) {
return [...new Set(arr)];
}
const numbers = [1, 2, 2, 3, 4, 4, 5];
console.log(uniqueArray(numbers)); // [1, 2, 3, 4, 5]
// 对象数组去重(基于特定属性)
function uniqueByKey(arr, key) {
const seen = new Set();
return arr.filter(item => {
const value = item[key];
if (seen.has(value)) {
return false;
}
seen.add(value);
return true;
});
}
数据验证
javascript
class FormValidator {
constructor() {
this.validators = new Set();
this.errors = new Set();
}
addValidator(validator) {
this.validators.add(validator);
}
validate(data) {
this.errors.clear();
for (let validator of this.validators) {
const error = validator(data);
if (error) {
this.errors.add(error);
}
}
return this.errors.size === 0;
}
getErrors() {
return [...this.errors];
}
}
// 使用示例
const validator = new FormValidator();
validator.addValidator(data => !data.email ? 'Email required' : null);
validator.addValidator(data => data.password?.length < 6 ? 'Password too short' : null);
事件管理
javascript
class EventEmitter {
constructor() {
this.listeners = new Map();
}
on(event, callback) {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event).add(callback);
}
off(event, callback) {
const callbacks = this.listeners.get(event);
if (callbacks) {
callbacks.delete(callback);
}
}
emit(event, data) {
const callbacks = this.listeners.get(event);
if (callbacks) {
callbacks.forEach(callback => callback(data));
}
}
}
6.7 WeakSet
WeakSet 是 Set 的"弱"版本,只能存储对象引用,且是弱引用。
6.7.1 基本API
javascript
const ws = new WeakSet();
const obj1 = { id: 1 };
const obj2 = { id: 2 };
ws.add(obj1);
ws.add(obj2);
ws.delete(obj2);
console.log(ws.has(obj1)); // true
6.7.2 弱值
WeakSet中的值不属于正式的引用,不会阻碍垃圾回收。
javascript
let user = { name: "John" };
const trackedUsers = new WeakSet();
trackedUsers.add(user);
// 当 user 不再被引用时,垃圾回收器可以回收这个对象
user = null; // 此时 { name: "John" } 可以被垃圾回收
//然后弱集合中的该值也会被销毁。
6.7.3 与 Set 的关键区别
|------|-----------|-----------|
| 特性 | WeakSet | Set |
| 值类型 | 只能是对象 | 任意类型 |
| 可迭代 | 否 | 是 |
| 可清空 | 否 | clear() |
| 大小 | 无 size 属性 | 有 size 属性 |
| 垃圾回收 | 值是弱引用 | 值是强引用 |
6.7.4 现代应用场景
对象标记
javascript
const processedObjects = new WeakSet();
function processObject(obj) {
// 检查是否已经处理过
if (processedObjects.has(obj)) {
console.log('Object already processed');
return;
}
// 处理对象...
console.log('Processing object:', obj);
// 标记为已处理
processedObjects.add(obj);
}
const data = { value: 42 };
processObject(data); // Processing object: {value: 42}
processObject(data); // Object already processed
DOM 元素跟踪
javascript
const clickedElements = new WeakSet();
document.addEventListener('click', function(event) {
const target = event.target;
// 防止重复处理同一元素的多次点击
if (clickedElements.has(target)) {
return;
}
// 标记元素已被点击
clickedElements.add(target);
// 执行点击处理逻辑
handleFirstClick(target);
});
function handleFirstClick(element) {
console.log('First click on:', element);
// 初始化操作...
}
私有成员检测
javascript
const internalInstances = new WeakSet();
class SecureAPI {
constructor(apiKey) {
this.apiKey = apiKey;
internalInstances.add(this);
}
makeRequest(data) {
// 确保只有通过构造函数创建的对象才能调用此方法
if (!internalInstances.has(this)) {
throw new Error('Invalid instance');
}
// 安全地执行请求...
console.log('Making secure request with:', data);
}
// 防止外部修改原型链
static create(apiKey) {
return new SecureAPI(apiKey);
}
}
6.7.5 实际项目中的使用建议
何时使用 Set
- 需要存储唯一值时
- 需要快速查找、删除操作时
- 进行集合运算(并集、交集等)时
- 管理不重复的列表数据时
何时使用 WeakSet
- 需要标记对象状态而不影响垃圾回收时
- 跟踪对象是否经过特定处理时
- 管理临时对象关联数据时
- 避免内存泄漏是关键考虑时