Langchain系列文章目录
01-玩转LangChain:从模型调用到Prompt模板与输出解析的完整指南
02-玩转 LangChain Memory 模块:四种记忆类型详解及应用场景全覆盖
03-全面掌握 LangChain:从核心链条构建到动态任务分配的实战指南
04-玩转 LangChain:从文档加载到高效问答系统构建的全程实战
05-玩转 LangChain:深度评估问答系统的三种高效方法(示例生成、手动评估与LLM辅助评估)
06-从 0 到 1 掌握 LangChain Agents:自定义工具 + LLM 打造智能工作流!
07-【深度解析】从GPT-1到GPT-4:ChatGPT背后的核心原理全揭秘
08-【万字长文】MCP深度解析:打通AI与世界的“USB-C”,模型上下文协议原理、实践与未来
Python系列文章目录
PyTorch系列文章目录
机器学习系列文章目录
深度学习系列文章目录
Java系列文章目录
JavaScript系列文章目录
01-【JavaScript-Day 1】从零开始:全面了解 JavaScript 是什么、为什么学以及它与 Java 的区别
02-【JavaScript-Day 2】开启 JS 之旅:从浏览器控制台到 <script>
标签的 Hello World 实践
03-【JavaScript-Day 3】掌握JS语法规则:语句、分号、注释与大小写敏感详解
04-【JavaScript-Day 4】var
完全指南:掌握变量声明、作用域及提升
05-【JavaScript-Day 5】告别 var
陷阱:深入理解 let
和 const
的妙用
06-【JavaScript-Day 6】从零到精通:JavaScript 原始类型 String, Number, Boolean, Null, Undefined, Symbol, BigInt 详解
07-【JavaScript-Day 7】全面解析 Number 与 String:JS 数据核心操作指南
08-【JavaScript-Day 8】告别混淆:一文彻底搞懂 JavaScript 的 Boolean、null 和 undefined
09-【JavaScript-Day 9】从基础到进阶:掌握 JavaScript 核心运算符之算术与赋值篇
10-【JavaScript-Day 10】掌握代码决策核心:详解比较、逻辑与三元运算符
11-【JavaScript-Day 11】避坑指南!深入理解JavaScript隐式和显式类型转换
12-【JavaScript-Day 12】掌握程序流程:深入解析 if…else 条件语句
13-【JavaScript-Day 13】告别冗长if-else:精通switch语句,让代码清爽高效!
14-【JavaScript-Day 14】玩转 for
循环:从基础语法到遍历数组实战
15-【JavaScript-Day 15】深入解析 while 与 do…while 循环:满足条件的重复执行
16-【JavaScript-Day 16】函数探秘:代码复用的基石——声明、表达式与调用详解
17-【JavaScript-Day 17】函数的核心出口:深入解析 return
语句的奥秘
18-【JavaScript-Day 18】揭秘变量的“隐形边界”:深入理解全局与函数作用域
19-【JavaScript-Day 19】深入理解 JavaScript 作用域:块级、词法及 Hoisting 机制
20-【JavaScript-Day 20】揭秘函数的“记忆”:深入浅出理解闭包(Closure)
21-【JavaScript-Day 21】闭包实战:从模块化到内存管理,高级技巧全解析
22-【JavaScript-Day 22】告别 function
关键字?ES6 箭头函数 (=>
) 深度解析
23-【JavaScript-Day 23】告别繁琐的参数处理:玩转 ES6 默认参数与剩余参数
24-【JavaScript-Day 24】从零到一,精通 JavaScript 对象:创建、访问与操作
25-【JavaScript-Day 25】深入探索:使用 for...in
循环遍历 JavaScript 对象属性
26-【JavaScript-Day 26】零基础掌握JavaScript数组:轻松理解创建、索引、长度和多维结构
27-【JavaScript-Day 27】玩转数组:push
, pop
, slice
, splice
等方法详解与实战
28-【JavaScript-Day 28】告别繁琐循环:forEach
, map
, filter
数组遍历三剑客详解
29-【JavaScript-Day 29】数组迭代进阶:掌握 reduce、find、some 等高阶遍历方法
30-【JavaScript-Day 30】ES6新特性:Set与Map,让你的数据管理更高效!
文章目录
前言
在 JavaScript ES6 标准之前,我们主要依赖数组(Array)和普通对象(Object)来存储和管理数据。数组用于存储有序的元素集合,而对象则用于存储键值对。然而,它们在某些特定场景下存在一些局限性。例如,普通对象的键只能是字符串或 Symbol 类型,且无法直接存储不重复的元素集合。为了弥补这些不足,ES6 引入了两种新的数据结构:Set
和 Map
。它们为处理特定类型的数据集合提供了更专业、更高效的解决方案。本篇文章将带你深入理解 Set
和 Map
的特性、用法及其与传统数据结构的差异,助你提升代码质量与开发效率。
一、Set:独一无二的集合
Set
对象允许你存储任何类型的唯一值,无论是原始值还是对象引用。它类似于数学中的集合概念,集合中的元素都是唯一的,不会重复。
1.1 什么是 Set?
1.1.1 Set 的定义与特性
Set
是一种新的数据结构,它有以下核心特性:
- 成员唯一性:
Set
中的成员的值都是唯一的,没有重复的值。如果你尝试添加一个已经存在的值,Set
不会做任何操作,也不会报错。 - 无序性(但可迭代时按插入顺序):从概念上讲,集合是无序的。但在 JavaScript 的
Set
实现中,元素在迭代时会按照其插入顺序排列。 - 任何类型的值:
Set
成员可以是任何数据类型,包括原始类型(数字、字符串、布尔等)和对象引用。
1.1.2 为何需要 Set?
在需要确保数据唯一性的场景下,Set
非常有用。例如:
- 数组去重:快速去除数组中的重复元素。
- 判断元素是否存在:高效地检查一个值是否已存在于某个集合中。
- 存储唯一标识符列表:如用户 ID 列表,确保每个 ID 只出现一次。
1.2 Set 的创建与基本操作
1.2.1 创建 Set
你可以通过 new Set()
构造函数来创建一个空的 Set
,或者传入一个可迭代对象(如数组)来初始化 Set
。
// 创建一个空的 Set
const mySet = new Set();
console.log(mySet); // Set(0) {}
// 通过数组初始化 Set (重复元素会被自动忽略)
const arr = [1, 2, 2, 3, 'hello', 'hello', true];
const uniqueSet = new Set(arr);
console.log(uniqueSet); // Set(5) { 1, 2, 3, 'hello', true }
1.2.2 核心方法
Set
实例提供了一些非常实用的方法来操作其成员:
(1) add(value)
:添加元素
向 Set
集合中添加一个新元素。如果元素已存在,则不进行任何操作。返回 Set
对象本身,因此可以链式调用。
const roles = new Set();
roles.add('admin');
roles.add('editor');
roles.add('admin'); // 尝试添加重复元素,无效
console.log(roles); // Set(2) { 'admin', 'editor' }
console.log(roles.add('viewer')); // Set(3) { 'admin', 'editor', 'viewer' } (返回 Set 对象)
(2) delete(value)
:删除元素
从 Set
集合中删除指定的元素。如果元素存在并被成功删除,则返回 true
;否则返回 false
。
const permissions = new Set(['read', 'write', 'execute']);
const result1 = permissions.delete('write');
console.log(result1); // true
console.log(permissions); // Set(2) { 'read', 'execute' }
const result2 = permissions.delete('update'); // 删除一个不存在的元素
console.log(result2); // false
(3) has(value)
:判断元素是否存在
判断 Set
集合中是否包含指定的元素。如果包含,则返回 true
;否则返回 false
。
const tags = new Set(['javascript', 'es6', 'web']);
console.log(tags.has('es6')); // true
console.log(tags.has('java')); // false
(4) clear()
:清空所有元素
移除 Set
对象内的所有元素。
const userIds = new Set([101, 102, 103]);
console.log(userIds.size); // 3
userIds.clear();
console.log(userIds.size); // 0
console.log(userIds); // Set(0) {}
1.2.3 size
属性
Set
实例拥有一个 size
属性,用于返回 Set
对象中值的数量(集合的基数)。
const numbers = new Set([1, 2, 3, 4, 5]);
console.log(numbers.size); // 5
1.2.4 遍历 Set
Set
对象是可迭代的,可以使用多种方式进行遍历:
(1) forEach()
方法
Set
实例的 forEach()
方法与数组的类似,但回调函数的参数略有不同:value
和 key
都是元素本身(因为 Set
没有键的概念,为了与 Map
的 forEach
接口保持一致),第三个参数是 Set
对象本身。
const colors = new Set(['red', 'green', 'blue']);
colors.forEach((value, key, set) => {
console.log(`Value: ${value}, Key (same as value): ${key}`);
// console.log(set === colors); // true
});
// 输出:
// Value: red, Key (same as value): red
// Value: green, Key (same as value): green
// Value: blue, Key (same as value): blue
(2) for...of
循环
这是遍历 Set
最简洁和推荐的方式。
const fruits = new Set(['apple', 'banana', 'orange']);
for (const fruit of fruits) {
console.log(fruit);
}
// 输出:
// apple
// banana
// orange
Set
实例还有 keys()
、values()
和 entries()
方法,它们都返回一个新的迭代器对象。对于 Set
而言:
keys()
返回一个包含集合中所有值的迭代器。values()
返回一个包含集合中所有值的迭代器 (行为与keys()
相同)。entries()
返回一个包含[value, value]
形式数组的迭代器,以模拟Map
的entries()
。
const letters = new Set(['a', 'b', 'c']);
console.log('Keys:');
for (const key of letters.keys()) {
console.log(key); // a, b, c
}
console.log('\nValues:');
for (const value of letters.values()) {
console.log(value); // a, b, c
}
console.log('\nEntries:');
for (const entry of letters.entries()) {
console.log(entry); // ['a', 'a'], ['b', 'b'], ['c', 'c']
}
1.3 Set 的应用场景
1.3.1 数组去重
这是 Set
最经典的应用之一。
const duplicateArray = [1, 1, 2, 3, 4, 4, 4, 5, 'a', 'b', 'a'];
const uniqueArray = [...new Set(duplicateArray)]; // 使用扩展运算符将 Set 转回数组
console.log(uniqueArray); // [ 1, 2, 3, 4, 5, 'a', 'b' ]
1.3.2 判断元素是否存在
利用 has()
方法可以高效判断。
const allowedUsers = new Set(['Alice', 'Bob', 'Charlie']);
const currentUser = 'Bob';
if (allowedUsers.has(currentUser)) {
console.log(`${currentUser} is allowed.`); // Bob is allowed.
} else {
console.log(`${currentUser} is not allowed.`);
}
1.3.3 计算交集、并集、差集 (示例)
虽然 Set
本身没有直接提供这些数学运算的方法,但可以很容易地基于其特性实现。
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:', union); // Set(6) { 1, 2, 3, 4, 5, 6 }
// 交集
const intersection = new Set([...setA].filter(x => setB.has(x)));
console.log('Intersection:', intersection); // Set(2) { 3, 4 }
// 差集 (A - B)
const difference = new Set([...setA].filter(x => !setB.has(x)));
console.log('Difference (A-B):', difference); // Set(2) { 1, 2 }
二、Map:灵活的键值对
Map
对象保存键值对,并且能够记住键的原始插入顺序。任何值(对象或者原始值)都可以作为一个键或一个值。这与普通对象(Object)有显著区别,普通对象的键只能是字符串或 Symbol
。
2.1 什么是 Map?
2.1.1 Map 的定义与特性
Map
是一种新的数据结构,它有以下核心特性:
- 键的类型多样性:
Map
的键可以是任何类型的值,包括函数、对象或任何原始类型。而普通对象的键只能是字符串或Symbol
。 - 有序性:
Map
中的键值对是按照插入顺序排列的,迭代时会遵循这个顺序。 - 键值对集合:存储的是键和值之间的映射关系。
2.1.2 为何需要 Map?
当需要使用非字符串类型的键,或者需要保持插入顺序的键值对集合时,Map
是更好的选择。
- 使用对象作为键:例如,将 DOM 节点作为键来存储其相关元数据。
- 保持插入顺序的字典:普通对象在 ES2015 之前不保证属性顺序。
- 更安全的键名:避免与对象原型链上的属性名冲突。
下面是一个简单的 Mermaid 图,概念性地展示了 Map
结构:
graph LR
subgraph MapInstance [Map: Key-Value Pairs]
K1[Key1 (any type)] --> V1[Value1 (any type)]
K2[Key2 (any type)] --> V2[Value2 (any type)]
K3[Key3 (any type)] --> V3[Value3 (any type)]
ObjKey{ "{id:1}" (Object) } --> OV["Associated Data"]
FuncKey[ myFunc (Function) ] --> FV["Function Metadata"]
end
2.2 Map 的创建与基本操作
2.2.1 创建 Map
你可以通过 new Map()
构造函数来创建一个空的 Map
,或者传入一个包含键值对的数组(或任何可迭代对象,其元素是二元数组 [key, value]
)来初始化 Map
。
// 创建一个空的 Map
const myMap = new Map();
console.log(myMap); // Map(0) {}
// 通过二维数组初始化 Map
const initialPairs = [
['name', 'Alice'],
[100, 'Score'],
[true, 'Is Active'],
[{id: 1}, 'User Object']
];
const userMap = new Map(initialPairs);
console.log(userMap);
// Map(4) {
// 'name' => 'Alice',
// 100 => 'Score',
// true => 'Is Active',
// { id: 1 } => 'User Object'
// }
2.2.2 核心方法
Map
实例提供了一系列方法来操作其键值对:
(1) set(key, value)
:设置键值对
向 Map
中添加或更新一个键值对。返回 Map
对象本身,因此可以链式调用。
const productMap = new Map();
productMap.set('id', 'P123');
productMap.set('name', 'Laptop');
productMap.set('price', 999);
const someObjectKey = { type: 'config' };
productMap.set(someObjectKey, { theme: 'dark', language: 'en' });
console.log(productMap.set('stock', 50)); // 返回 Map 对象
console.log(productMap);
// Map(4) {
// 'id' => 'P123',
// 'name' => 'Laptop',
// 'price' => 999,
// { type: 'config' } => { theme: 'dark', language: 'en' },
// 'stock' => 50
// }
(2) get(key)
:获取值
根据键获取 Map
中对应的值。如果键不存在,则返回 undefined
。
console.log(productMap.get('name')); // 'Laptop'
console.log(productMap.get({ type: 'config' })); // undefined (因为是不同的对象引用)
console.log(productMap.get(someObjectKey)); // { theme: 'dark', language: 'en' } (相同的对象引用)
console.log(productMap.get('color')); // undefined (键不存在)
注意:当使用对象作为键时,Map
比较的是对象的引用。除非是同一个对象引用,否则即使两个对象结构相同,它们在 Map
中也被视为不同的键。
(3) has(key)
:判断键是否存在
判断 Map
中是否存在指定的键。如果存在,则返回 true
;否则返回 false
。
console.log(productMap.has('price')); // true
console.log(productMap.has('description')); // false
console.log(productMap.has(someObjectKey)); // true
(4) delete(key)
:删除键值对
从 Map
中删除指定的键及其关联的值。如果键存在并被成功删除,则返回 true
;否则返回 false
。
const result1 = productMap.delete('stock');
console.log(result1); // true
console.log(productMap.has('stock')); // false
const result2 = productMap.delete('nonExistentKey');
console.log(result2); // false
(5) clear()
:清空所有键值对
移除 Map
对象内的所有键值对。
console.log(productMap.size); // 3 (假设 stock 已被删除)
productMap.clear();
console.log(productMap.size); // 0
console.log(productMap); // Map(0) {}
2.2.3 size
属性
Map
实例的 size
属性返回 Map
对象中键值对的数量。
const dataMap = new Map([['a', 1], ['b', 2]]);
console.log(dataMap.size); // 2
2.2.4 遍历 Map
Map
对象是可迭代的,并且提供了多种遍历方式:
(1) forEach()
方法
Map
实例的 forEach()
方法回调函数的参数是 value
、key
和 map
对象本身。
const settings = new Map([
['theme', 'dark'],
['fontSize', 16],
['notifications', true]
]);
settings.forEach((value, key, map) => {
console.log(`Key: ${key}, Value: ${value}`);
// console.log(map === settings); // true
});
// 输出:
// Key: theme, Value: dark
// Key: fontSize, Value: 16
// Key: notifications, Value: true
(2) for...of
循环 (结合 entries()
, keys()
, values()
)
-
遍历键值对 (entries):
map.entries()
或直接for...of map
console.log('\nIterating over entries:'); for (const entry of settings.entries()) { // settings.entries() 可以简写为 settings console.log(entry); // ['theme', 'dark'], ['fontSize', 16], ['notifications', true] console.log(`Key: ${entry[0]}, Value: ${entry[1]}`); } // 或者使用解构赋值 console.log('\nIterating over entries with destructuring:'); for (const [key, value] of settings) { console.log(`Key: ${key}, Value: ${value}`); }
-
遍历键 (keys):
map.keys()
console.log('\nIterating over keys:'); for (const key of settings.keys()) { console.log(key); // theme, fontSize, notifications }
-
遍历值 (values):
map.values()
console.log('\nIterating over values:'); for (const value of settings.values()) { console.log(value); // dark, 16, true }
2.3 Map 的应用场景
2.3.1 将对象作为键
当需要将 DOM 元素或其他对象与特定数据关联起来时,Map
非常有用。
const domElement1 = document.createElement('div');
const domElement2 = document.createElement('span');
const elementData = new Map();
elementData.set(domElement1, { id: 'elem1', eventCount: 0 });
elementData.set(domElement2, { id: 'elem2', eventCount: 5 });
console.log(elementData.get(domElement1)); // { id: 'elem1', eventCount: 0 }
// 假设有一个点击事件处理器
function handleClick(event) {
const data = elementData.get(event.target);
if (data) {
data.eventCount++;
console.log(`${data.id} clicked ${data.eventCount} times.`);
}
}
// domElement1.addEventListener('click', handleClick); // 示例用法
2.3.2 存储配置或元数据
Map
可以清晰地存储具有不同类型键的配置信息。
const appConfig = new Map();
appConfig.set('version', '1.0.5');
appConfig.set('debugMode', true);
appConfig.set(Symbol('apiKey'), 'xyz123abc'); // 使用 Symbol 作为键
appConfig.set(window, { title: 'Main Window' }); // 使用 window 对象作为键
console.log(appConfig.get('version')); // '1.0.5'
三、Map/Set 与 Object/Array 的比较
理解 Map
、Set
与传统的 Object
、Array
之间的差异,有助于我们在合适的场景选择最恰当的数据结构。
3.1 Set 与 Array 的比较
特性 | Set | Array |
---|---|---|
存储内容 | 唯一的、无重复的值 | 可以包含重复的值 |
键/索引 | 无传统意义上的索引 | 通过数字索引 (0, 1, 2…) 访问元素 |
顺序性 | 迭代时按插入顺序 | 元素按索引顺序排列 |
查找效率 | has() 方法通常比 Array.includes() 或 indexOf() 更快 (尤其在大数据量时) | includes() , indexOf() 效率相对较低 |
主要用途 | 确保值的唯一性,快速判断成员是否存在 | 存储有序的元素列表,常用于需要索引访问的场景 |
常用操作 | add() , delete() , has() , size | push() , pop() , splice() , length |
何时选择 Set
而不是 Array
?
- 当你需要存储一系列不重复的值时。
- 当你需要频繁检查一个值是否已存在于集合中时。
- 当你需要进行集合运算(如并集、交集、差集)的基础时。
3.2 Map 与 Object 的比较
特性 | Map | Object (Plain Object) |
---|---|---|
键的类型 | 任何类型的值 (原始类型、对象、函数等) | 只能是字符串 (String) 或符号 (Symbol) |
顺序性 | 键值对按插入顺序迭代 | 在 ES2015 之前,属性顺序不保证;ES2015+ 对某些情况有规定,但仍不如 Map 明确 |
大小获取 | size 属性,直接且高效 | 通常通过 Object.keys(obj).length 获取,间接 |
迭代 | 直接可迭代 (for...of ),提供 keys() , values() , entries() | 需要 for...in (会遍历原型链属性,需 hasOwnProperty 过滤) 或 Object.keys() , Object.values() , Object.entries() |
原型 | 干净,不继承任何属性,除非显式从 Map.prototype 继承 | 继承自 Object.prototype (除非用 Object.create(null) ),可能导致键名冲突 |
性能 | 在频繁增删键值对的场景下,通常比 Object 表现更好 | 对于固定数量的属性,访问速度可能非常快 |
何时选择 Map
而不是 Object
?
- 当你需要使用非字符串/Symbol 类型的键时(例如,用 DOM 元素作为键)。
- 当你需要严格保证键值对的插入顺序时。
- 当键值对数量会频繁变动,且需要高效地获取大小时。
- 当你希望避免原型链带来的潜在键名冲突时。
3.3 何时选择?(决策流程图思路)
下面是一个简化的决策思路,帮助你选择合适的数据结构,可以用 Mermaid 流程图来形象化表达:
这个流程图提供了一个大致的思考方向。实际开发中,选择还需结合具体业务需求、性能考量和团队编码习惯。
四、总结
Set
和 Map
是 ES6 中引入的强大且实用的数据结构,它们为 JavaScript 开发者提供了更丰富的工具来处理不同类型的数据集合和映射关系。
-
Set
的核心价值在于其成员的唯一性:- 它非常适合用于数组去重、判断成员是否存在等场景。
- 提供
add()
,delete()
,has()
,clear()
等简洁的 API 和size
属性。 - 可直接使用
for...of
或forEach()
进行迭代。
-
Map
的核心价值在于其键的灵活性和有序性:- 它允许任何类型的值作为键,弥补了普通对象键类型的限制。
- 键值对按插入顺序存储和迭代。
- 提供
set()
,get()
,has()
,delete()
,clear()
等 API 和size
属性。 - 同样支持
for...of
和forEach()
迭代,并提供keys()
,values()
,entries()
方法。
-
选择合适的结构:
- 当处理唯一值集合时,优先考虑
Set
。 - 当需要灵活的键类型或有序的键值对时,优先考虑
Map
。 - 传统的
Array
和Object
仍然是许多场景下的首选,特别是当Array
的索引访问和Object
的简单字符串键特性符合需求时。
- 当处理唯一值集合时,优先考虑
掌握 Set
和 Map
的使用,能够让我们编写出更简洁、更高效、更易于维护的 JavaScript 代码。在日常开发中,根据具体需求选择最合适的数据结构,是提升代码质量的关键一步。