在 ES6 之前,JavaScript 中用于存储键值对的主要数据结构是对象(Object)。但对象存在一些固有的局限性,比如键只能是字符串或 Symbol 类型、无法直接获取键值对数量、遍历方式不够灵活等。为了解决这些问题,ES6 引入了 Map 数据结构,它是一种更加强大、灵活的键值对集合。本文将从基础到实战,全面解析 Map 的核心知识点,帮助你彻底掌握这个实用的工具。
一、Map 是什么?核心特性速览
Map 是 ES6 新增的内置对象,用于存储键值对(key-value pairs),并且允许任何类型的值(包括原始类型、对象、函数等)作为键(key),这是它与对象最核心的区别之一。
Map 的核心特性总结:
-
键的多样性:键可以是字符串、数字、布尔值、null、undefined、Symbol、对象、函数等,突破了对象键只能是字符串/Symbol 的限制。
-
有序性:Map 中的键值对是按照插入顺序排列的,遍历的时候会按照插入顺序返回结果,而对象在 ES6 之前是无序的(ES6 后对象键有一定排序规则,但不如 Map 明确)。
-
可迭代性:Map 本身是可迭代对象,可以直接通过 for...of 循环遍历,无需像对象那样借助 Object.keys() 等方法。
-
动态获取长度:通过 size 属性可以直接获取 Map 中键值对的数量,而对象需要通过 Object.keys(obj).length 计算。
-
无原型链干扰:Map 没有原型链,不会出现像对象那样因原型链上的属性而导致的键名冲突(比如 obj.hasOwnProperty() 才能判断自身属性)。
二、Map 的基本使用:创建与初始化
创建 Map 实例主要有两种方式:通过构造函数直接创建空 Map,或者传入可迭代对象(如数组)初始化 Map。
2.1 方式一:创建空 Map 并添加键值对
使用 Map() 构造函数创建空实例后,通过 set() 方法添加键值对。
javascript
// 创建空 Map
const map = new Map();
// 添加键值对,支持不同类型的键
map.set('name', '张三'); // 字符串作为键
map.set(18, '年龄'); // 数字作为键
map.set(true, '是否成年'); // 布尔值作为键
map.set({ id: 1 }, '用户对象'); // 对象作为键
map.set(() => 'hello', '函数作为键'); // 函数作为键
console.log(map);
// Map(5) { 'name' => '张三', 18 => '年龄', true => '是否成年', { id: 1 } => '用户对象', [Function (anonymous)] => '函数作为键' }
2.2 方式二:通过可迭代对象初始化
Map 构造函数可以接收一个可迭代对象(如二维数组)作为参数,数组中的每个元素是一个包含"键"和"值"的二维数组 [key, value]。
javascript
// 用二维数组初始化 Map
const map = new Map([
['name', '李四'],
[Symbol('id'), 1001], // Symbol 作为键
[null, '空值键'],
[undefined, '未定义键']
]);
console.log(map.size); // 4,通过 size 属性获取长度
console.log(map.get('name')); // 李四,通过 get() 方法获取值
三、Map 的核心 API:增删改查与遍历
Map 提供了一套完善的 API 用于操作键值对和遍历,掌握这些 API 是使用 Map 的基础。
3.1 操作键值对的核心方法
| 方法 | 作用 | 示例 |
|---|---|---|
| set(key, value) | 添加/修改键值对(键存在则修改值,不存在则新增),返回 Map 实例(可链式调用) | map.set('age', 20).set('gender', '男') |
| get(key) | 根据键获取值,键不存在则返回 undefined | map.get('name') // 张三 |
| has(key) | 判断键是否存在,返回布尔值 | map.has(18) // true |
| delete(key) | 删除指定键的键值对,成功删除返回 true,键不存在返回 false | map.delete('name') // true |
| clear() | 清空 Map 中所有键值对,无返回值 | map.clear() |
3.2 关键属性:size
size 属性用于获取 Map 中键值对的数量,注意是属性而非方法,不需要加括号。
javascript
const map = new Map([['a', 1], ['b', 2]]);
console.log(map.size); // 2
map.delete('a');
console.log(map.size); // 1
map.clear();
console.log(map.size); // 0
3.3 遍历 Map 的四种方式
Map 是可迭代对象,支持四种遍历方式,且遍历顺序均为插入顺序。
1. 遍历键值对:for...of 直接遍历 Map
直接遍历 Map 时,每次迭代返回的是 [key, value] 形式的数组。
javascript
const map = new Map([['name', '王五'], ['age', 22]]);
for (const [key, value] of map) {
console.log(`${key}: ${value}`);
}
// 输出:
// name: 王五
// age: 22
2. 遍历键:keys() 方法
keys() 方法返回一个键的迭代器对象,可通过 for...of 遍历。
javascript
for (const key of map.keys()) {
console.log('键:', key);
}
// 输出:
// 键: name
// 键: age
3. 遍历值:values() 方法
values() 方法返回一个值的迭代器对象,可通过 for...of 遍历。
javascript
for (const value of map.values()) {
console.log('值:', value);
}
// 输出:
// 值: 王五
// 值: 22
4. 遍历键值对:forEach() 方法
forEach() 方法接收一个回调函数,回调参数依次为:value(值)、key(键)、map(当前 Map 实例)。
javascript
map.forEach((value, key, currentMap) => {
console.log(`${key}: ${value}`);
console.log(currentMap === map); // true
});
// 输出:
// name: 王五
// true
// age: 22
// true
四、Map 的关键细节:你必须知道的"坑"
使用 Map 时,有一些细节容易出错,尤其是键的比较规则和引用类型键的处理。
4.1 键的比较规则:"SameValueZero" 算法
Map 中判断两个键是否相等采用的是 "SameValueZero" 算法,与 === 运算符类似,但有两个区别:
-
NaN 与 NaN 相等(=== 中 NaN === NaN 为 false);
-
+0 与 -0 相等(=== 中 +0 === -0 为 true,两者一致)。
javascriptconst map = new Map(); // NaN 作为键,多次添加会覆盖 map.set(NaN, '第一个 NaN'); map.set(NaN, '第二个 NaN'); console.log(map.get(NaN)); // 第二个 NaN // +0 和 -0 视为同一个键 map.set(+0, '正零'); map.set(-0, '负零'); console.log(map.get(+0)); // 负零
4.2 引用类型键的"引用相等"
如果键是引用类型(如对象、数组、函数),Map 判断键是否相等的依据是引用地址是否相同,而非值是否相同。
javascript
const map = new Map();
const obj1 = { id: 1 };
const obj2 = { id: 1 };
// 虽然 obj1 和 obj2 的值相同,但引用地址不同,视为两个不同的键
map.set(obj1, '对象1');
map.set(obj2, '对象2');
console.log(map.get(obj1)); // 对象1
console.log(map.get(obj2)); // 对象2
console.log(map.size); // 2
这一点在实际开发中很容易踩坑,比如用对象作为键存储数据时,必须使用同一个对象引用才能获取到对应的值。
五、Map 与 Object、Set 的区别
为了更清晰地理解 Map 的定位,我们对比它与 Object、Set 的核心区别。
5.1 Map vs Object
| 对比项 | Map | Object |
|---|---|---|
| 键的类型 | 任意类型(原始值、引用值) | 仅字符串、Symbol、数字(会自动转为字符串) |
| 有序性 | 插入顺序,可预测 | ES6 后有排序规则(数字优先、字符串按插入顺序),不直观 |
| 长度获取 | size 属性直接获取 | 需通过 Object.keys(obj).length 计算 |
| 遍历方式 | for...of、forEach 等,直接遍历 | 需借助 Object.keys() 等方法,遍历自身属性需判断 hasOwnProperty |
| 原型链干扰 | 无原型链,无干扰 | 有原型链,可能存在键名冲突(如 toString) |
5.2 Map vs Set
Set 也是 ES6 新增的集合类型,但与 Map 定位不同:
-
Map:存储键值对(key-value),核心是"映射关系";
-
Set:存储唯一值(value),核心是"去重集合"。
六、Map 的实际应用场景
Map 因其特性,在很多场景下比 Object 更合适,以下是常见的应用场景:
6.1 存储多类型键的映射关系
当需要用非字符串类型(如数字、对象、Symbol)作为键时,Map 是唯一选择。例如,用 DOM 元素作为键存储对应的状态:
javascript
const btnStatus = new Map();
const btn1 = document.getElementById('btn1');
const btn2 = document.getElementById('btn2');
// 用 DOM 元素作为键,存储按钮的禁用状态
btnStatus.set(btn1, false);
btnStatus.set(btn2, true);
// 后续获取状态
if (btnStatus.get(btn1)) {
btn1.disabled = true;
}
6.2 需要保持插入顺序的键值对集合
当需要遍历键值对时保持插入顺序(如配置项、日志记录),Map 比 Object 更可靠。例如,存储用户操作日志,按操作顺序遍历:
javascript
const operationLog = new Map();
// 按操作顺序插入日志
operationLog.set(Date.now(), '用户登录');
operationLog.set(Date.now(), '查看商品');
operationLog.set(Date.now(), '提交订单');
// 按插入顺序遍历日志
for (const [time, action] of operationLog) {
console.log(`[${new Date(time).toLocaleString()}] ${action}`);
}
6.3 频繁增删且需要快速获取长度的场景
Map 的 size 属性获取长度是 O(1) 时间复杂度,而 Object 需要遍历计算,频繁增删时 Map 性能更优。例如,购物车商品管理:
javascript
const cart = new Map();
// 添加商品
function addToCart(goodsId, name, price) {
if (cart.has(goodsId)) {
// 已存在则数量+1
const goods = cart.get(goodsId);
cart.set(goodsId, { ...goods, count: goods.count + 1 });
} else {
cart.set(goodsId, { name, price, count: 1 });
}
}
// 删除商品
function removeFromCart(goodsId) {
cart.delete(goodsId);
}
// 获取购物车商品数量(O(1) 操作)
function getCartCount() {
return cart.size;
}
6.4 避免原型链污染的场景
当需要存储动态键名且担心与 Object 原型链属性冲突时,Map 更安全。例如,存储用户输入的键值对(用户可能输入"toString"等原型属性名):
javascript
// 用 Object 可能污染原型
const userData = {};
userData.toString = '恶意值';
console.log({}.toString()); // 函数,未被污染?实际在严格模式或现代环境中会限制,但仍有风险
// 用 Map 完全无风险
const safeUserData = new Map();
safeUserData.set('toString', '恶意值');
console.log(safeUserData.get('toString')); // 恶意值,不影响原型
七、总结
Map 是 ES6 为解决 Object 局限性而设计的键值对集合,它支持多类型键、有序性、可迭代性和高效的增删查操作,在很多场景下比 Object 更优秀。但 Map 并非完全替代 Object,当键仅为字符串且不需要有序性时,Object 仍有简洁的语法优势。
核心要点回顾:
-
Map 支持任意类型键,判断键相等用 SameValueZero 算法;
-
size 属性获取长度,set/get/has/delete/clear 操作键值对;
-
四种遍历方式,均保持插入顺序;
-
适合多类型键、有序性、频繁增删的场景,避免原型链污染。
掌握 Map 的使用,能让你的 JavaScript 代码更灵活、高效,尤其是在复杂场景下提升开发效率和代码可靠性。