精读《JavaScript 高级程序设计 第4版》第6章 集合引用类型(三)Map、WeakMap、Set、WeakSet

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
  • 需要标记对象状态而不影响垃圾回收时
  • 跟踪对象是否经过特定处理时
  • 管理临时对象关联数据时
  • 避免内存泄漏是关键考虑时
相关推荐
@LetsTGBot搜索引擎机器人7 小时前
打造属于你的 Telegram 中文版:汉化方案 + @letstgbot 搜索引擎整合教程
开发语言·python·搜索引擎·机器人·.net
十八朵郁金香7 小时前
【H5工具】一个简约高级感渐变海报H5设计工具
前端·javascript·产品运营·axure·个人开发
人工智能的苟富贵7 小时前
使用 Tauri + Rust 构建跨平台桌面应用:前端技术的新边界
开发语言·前端·rust·electron
j_xxx404_8 小时前
C++ STL:string类(3)|operations|string类模拟实现|附源码
开发语言·c++
拉不动的猪8 小时前
多窗口数据实时同步常规方案举例
前端·javascript·vue.js
GHZero8 小时前
Java 之解读String源码(九)
java·开发语言
Swift社区8 小时前
Lombok 不生效 —— 从排查到可运行 Demo(含实战解析)
java·开发语言·安全
南清的coding日记8 小时前
Java 程序员的 Vue 指南 - Vue 万字速览(01)
java·开发语言·前端·javascript·vue.js·css3·html5
@大迁世界8 小时前
我用 Rust 重写了一个 Java 微服务,然后丢了工作
java·开发语言·后端·微服务·rust