JS怎么创建Map对象及基本用法
本文详细讲解JavaScript中如何创建Map对象及其实用基础操作,包括构造函数传参、增删改查API、任意类型键的引用机制、遍历方法以及与普通Object的关键差异与避坑指南。
Map 是 ES6(ECMAScript 2015)引入的标准键值对集合结构。与传统的普通对象相比,Map 允许使用任何类型的值(包括函数、对象引用、NaN 以及原始类型)作为键,并严格按照插入顺序记录所有条目。本文将系统梳理 Map 对象的实例化方法、基础增删改查操作、特殊键的比较机制、多种遍历消费途径,以及它与普通 Object 的核心差异与开发避坑要点。
一、如何创建 Map 对象
创建 Map 实例必须通过原生的 Map 构造函数配合 new 关键字完成。根据具体的数据来源与初始化时机,通常有两种常见的构造方式。
1. 创建一个空 Map
最直接的方式是不传入任何参数直接调用 new Map(),这将生成一个空的键值对集合,其初始大小为 0。随后可以通过 API 按需逐步写入数据:
// 实例化一个空的 Map 容器
const userMap = new Map();
console.log(userMap.size); // 输出示例: 0
console.log(userMap); // 输出示例: Map(0) {}
2. 使用二维数组或可迭代对象预充数据
如果已有初始数据,Map 构造函数可以接收一个符合可迭代协议(Iterable)的参数。该参数中的每一个元素必须是包含两个元素的对偶结构 [key, value],最典型的就是“包含键值对子数组的二维数组”:
// 传入二维数组进行初始化批量注值
const statusMap = new Map([
['pending', '等待处理'],
['fulfilled', '已成功完成'],
['rejected', '发生错误已拒绝']
]);
console.log(statusMap.size); // 输出示例: 3
console.log(statusMap.get('fulfilled')); // 输出示例: "已成功完成"
除了手动书写的二维数组,还可以使用任何返回 [key, value] 序列的迭代器。例如,借助 Object.entries() 可以直接将现有的普通对象转换为 Map 实例;亦或是将现有的另一个 Map 传入以完成浅拷贝:
// 1. 将现有 Object 转换为 Map
const configObj = { host: 'localhost', port: 8080 };
const configMap = new Map(Object.entries(configObj));
// 2. 基于现有 Map 进行浅拷贝克隆
const clonedMap = new Map(configMap);
console.log(clonedMap.get('port')); // 输出示例: 8080
new Map(iterable) 的子项必须是二元集合。如果传递类似 ['key', 'value'] 的一维扁平数组,引擎会尝试将每个字符串字符拆解为两项,从而产生非预期的键值切分甚至抛出 TypeError: Iterator value is not an entry object 异常。
二、Map 的基础增删改查操作
Map 实例提供了专门的原型方法来完成全生命周期的数据管理,避免了普通对象通过属性名直接点操作带来的键名类型限制。
1. 写入与更新:map.set(key, value)
set() 方法接收两个参数:键(key)与值(value)。如果指定的键在 Map 中尚未存在,则会在末尾新增一条记录;如果该键已经存在,则直接覆盖原有的 value:
const cache = new Map();
// 写入普通原始类型键
cache.set('token', 'abc-123456');
cache.set(101, '管理员编号');
// 更新已有键的值
cache.set('token', 'xyz-987654');
console.log(cache.get('token')); // 输出示例: "xyz-987654"
特别地,map.set() 会返回当前 Map 实例自身,因此可以非常自然地使用链式调用(Fluent API)连续追加多个条目:
const routeMap = new Map()
.set('/home', '首页组件')
.set('/about', '关于页面组件')
.set('/dashboard', '控制台面板');
console.log(routeMap.size); // 输出示例: 3
2. 读取数据:map.get(key)
通过指定的键提取对应的 value。如果查找成功,返回对应的关联值;如果该键不存在,则返回 undefined:
console.log(routeMap.get('/home')); // 输出示例: "首页组件"
console.log(routeMap.get('/not-found')); // 输出示例: undefined
3. 判断键是否存在:map.has(key)
在普通对象中,如果属性值为 undefined,通过 obj[key] !== undefined 容易引发误判。而 Map 的 has() 方法专门用于断言该键是否存在于集合内部,返回布尔值:
const flags = new Map();
flags.set('debug', undefined); // 显式存入 undefined
console.log(flags.get('debug')); // 输出示例: undefined
console.log(flags.has('debug')); // 输出示例: true (键确实存在)
console.log(flags.has('trace')); // 输出示例: false(键不存在)
4. 删除单项:map.delete(key)
删除指定的键及其关联值。如果该键原本存在并被顺利移除,返回 true;若该键原本就不存在,返回 false:
const session = new Map([['sessionId', 's-9901']]);
const res1 = session.delete('sessionId');
console.log(res1); // 输出示例: true
const res2 = session.delete('sessionId');
console.log(res2); // 输出示例: false (已被删,无需重复移除)
5. 清空集合与获取尺寸:map.clear() 与 map.size
Map 拥有内置的只读属性 size,底层采用常量时间 $O(1)$ 维护,能即时返回当前键值对总数。当需要清空集合时,调用 clear() 即可瞬间释放所有条目:
const pool = new Map([['a', 1], ['b', 2]]);
console.log(pool.size); // 输出示例: 2
pool.clear();
console.log(pool.size); // 输出示例: 0
三、Map 键的特殊机制与比较算法
理解 Map 的核心在于理解它的键等价判定机制。在 ECMAScript 规范中,Map 内部采用 SameValueZero 算法来判定两个 key 是否相同:
1. 任何类型皆可作为键
与只能用字符串或 Symbol 作为键的普通 Object 不同,Map 可以接受 DOM 节点、函数、对象、甚至布尔值作为合法的键:
const metaStore = new Map();
const domBtn = { id: 'submit-button', tag: 'BUTTON' }; // 模拟 DOM 节点
const handlerFunc = function () { return 'clicked'; };
metaStore.set(domBtn, { clickCount: 12, enabled: true });
metaStore.set(handlerFunc, '事件处理器元数据');
console.log(metaStore.get(domBtn)); // 输出示例: { clickCount: 12, enabled: true }
console.log(metaStore.get(handlerFunc)); // 输出示例: "事件处理器元数据"
2. 引用相等性(Reference Equality)
当使用对象、数组或函数作为键时,判定依据是内存地址指针是否严格一致,而不是字面量内容是否相同:
const map = new Map();
// 常见误区:使用字面量对象写入
map.set({ id: 1 }, '用户数据');
// 尝试用字面量对象读取(失败)
console.log(map.get({ id: 1 })); // 输出示例: undefined
// 正确做法:保留对该对象的变量引用
const userObj = { id: 1 };
map.set(userObj, '有效保存的数据');
console.log(map.get(userObj)); // 输出示例: "有效保存的数据"
因为 { id: 1 } === { id: 1 } 为 false,两次创建的对象在堆内存中位于不同的内存空间,因此无法命中。
3. 特殊数值判定:NaN 与 0
在 JavaScript 原生运算符中,NaN === NaN 永远为 false。但在 Map 的 SameValueZero 算法下,所有的 NaN 都被视为完全相等的同一个键:
const mathMap = new Map();
mathMap.set(NaN, '计算异常标度');
mathMap.set(Number('invalid'), '再次写入');
console.log(mathMap.size); // 输出示例: 1 (键被合并覆盖)
console.log(mathMap.get(NaN)); // 输出示例: "再次写入"
// +0 与 -0 在 Map 中也被视作同一个键
mathMap.set(+0, '正零');
mathMap.set(-0, '负零');
console.log(mathMap.get(0)); // 输出示例: "负零"
四、Map 的遍历与结构转换
Map 实例自带原生的迭代器接口,并且其内部保证完全遵循条目被写入的先后顺序进行遍历输出。
1. 遍历键、值与键值对
Map 提供了三个迭代器发生器:keys()、values() 以及 entries():
const inventory = new Map([
['apple', 10],
['banana', 25],
['orange', 18]
]);
// 1. 只遍历键名
for (const key of inventory.keys()) {
console.log(key); // 按序输出: apple -> banana -> orange
}
// 2. 只遍历值
for (const val of inventory.values()) {
console.log(val); // 按序输出: 10 -> 25 -> 18
}
// 3. 遍历键值对 (支持数组解构)
for (const [fruit, count] of inventory.entries()) {
console.log(`${fruit}: ${count} 个`);
}
注意:inventory[Symbol.iterator]() 的默认实现即为 entries(),因此在 for...of 语句中直接遍历 Map 实例等同于遍历 map.entries():
for (const [key, val] of inventory) {
// 直接遍历 map 等同于 map.entries()
}
2. forEach 迭代回调
Map 也具备与 Array 类似的 forEach 高阶方法。需要特别注意参数顺序:第一个参数是 value,第二个参数才是 key,第三个参数是 map 实例本身(与数组的 (item, index) 设计思想保持统一):
inventory.forEach((value, key) => {
console.log(`键名: ${key}, 对应值: ${value}`);
});
3. 与数组及 JSON 对象的相互转换
在日常开发中,常需要将 Map 转化为纯数据格式输出到前端模板或网络接口:
const myMap = new Map([['name', 'Tom'], ['role', 'dev']]);
// 1. 转为二维数组
const arr = Array.from(myMap); // 或 [...myMap]
// 结果: [['name', 'Tom'], ['role', 'dev']]
// 2. 转为普通 Object (键必须是字符串或能转为字符串)
const plainObj = Object.fromEntries(myMap);
// 结果: { name: 'Tom', role: 'dev' }
// 3. JSON 序列化规范
// 直接 JSON.stringify(myMap) 将得到 "{}" 空对象!必须先转格式:
const jsonString = JSON.stringify(Object.fromEntries(myMap));
console.log(jsonString); // 输出示例: "{"name":"Tom","role":"dev"}"
五、Map 与普通 Object 核心差异对比
在 JavaScript 演进史中,很多开发者习惯把普通 {} 当作散列表(Hash Map)使用。然而在处理动态字典、频繁增删以及非字符串键的场景下,两者的底层表现存在实质性分水岭。
| 特性对比 | Map 集合对象 | 普通 JavaScript Object |
|---|---|---|
| 键类型支持 | 允许任意类型(对象、函数、数字、DOM、布尔值等)。 | 仅支持 String 或 Symbol,其余类型自动执行 toString() 转换。 |
| 键名遍历顺序 | 严格保持条目写入时的插入时间次序。 | 遵循复杂规则:非负整数优先升序,随后是字符串键按插入序,最后是 Symbol。 |
| 尺寸获取 | 原生提供 map.size 属性,$O(1)$ 瞬时读取。 |
无原生属性,必须通过 Object.keys(obj).length 遍历计算,耗时 $O(n)$。 |
| 默认原型干扰 | 纯净哈希结构,不含任何默认键,无命名冲突。 | 默认继承自 Object.prototype,自带 toString、constructor 等属性。 |
| 高频增删性能 | 底层专为频繁写入与删除优化,增删效率高且稳定。 | 高频使用 delete obj[k] 会破坏 V8 引擎的 HiddenClass 隐藏类优化。 |
选择准则:
- 优先使用 Map 的场景:构建缓存字典、高频执行动态键的插入与删除、键名需要携带对象或数字类型、要求严格按照录入顺序展示或遍历数据、需要频繁读取集合总容量。
- 优先使用 Object 的场景:定义具有固定结构的领域业务模型(如用户实体的
{ name, email, age })、需要直接与现有第三方库进行 JSON 序列化通信、仅需极少几个静态预定义配置项。
六、高频使用陷阱与避坑指南
虽然 Map 功能强大且易用,但在实际编码中仍有三个常见陷阱容易造成静默 Bug:
1. 误用方括号 map[key] 存取数据
在 Map 实例上使用 map['key'] = 'val' 语法时,由于 Map 本身也是一个 JavaScript 对象,这实际上是将值挂载到了该 Map 对象的普通属性上,完全绕过了 Map 内部的键值对存储机制:
const brokenMap = new Map();
// 错误操作:使用普通对象语法赋值
brokenMap['test'] = 123;
console.log(brokenMap.get('test')); // 输出示例: undefined (无法读取!)
console.log(brokenMap.size); // 输出示例: 0 (size 属性完全没变!)
console.log(brokenMap.has('test')); // 输出示例: false
.set(key, val) 与 .get(key) 原型方法,切忌混淆普通对象属性语法。
2. 对象字面量作为键的引用失效
如前文所述,对象作为键是通过引用地址比较的。如果每次调用 set() 或 get() 时都传递新生成的匿名对象字面量,将永远无法命中旧记录:
const userTokenMap = new Map();
// 错误写法:每次传入新的匿名对象
userTokenMap.set({ id: 100 }, 'TOKEN_A');
console.log(userTokenMap.has({ id: 100 })); // 输出示例: false
// 正确写法:在变量中牢牢固化对象引用
const currentUser = { id: 100 };
userTokenMap.set(currentUser, 'TOKEN_A');
console.log(userTokenMap.has(currentUser)); // 输出示例: true
3. JSON 序列化直接输出空对象的困惑
原生 JSON.stringify() 规范只针对普通对象的自有可枚举属性进行序列化,它无法识别 Map 内部的数据桶结构。直接执行 JSON.stringify(myMap) 只会得到 "{}"。遇到序列化需求时,应使用 Object.fromEntries(myMap)(适用于字符串键)或 Array.from(myMap)(保留任意类型键)作为中转桥梁。
七、总结
创建并熟练使用 Map 是编写高质量现代 JavaScript 代码的基础功底。归纳起来,掌握 Map 只需把握四个核心:
- 创建入口:使用
new Map()或向其传递二维键值数组new Map([[k, v], ...])。 - 核心接口:存取必须通过
set()与get(),断言用has(),剔除用delete(),清空用clear(),读大小用size。 - 键的自由度:拥抱任何类型作为键,但务必区分基本类型的“值比较”与对象类型的“引用地址比较”。
- 流转转换:遍历遵循插入序;需要与 JSON 交互时,依托
Array.from()或Object.fromEntries()建立桥梁。
ES6模块化中js文件怎么用import和export交互
2026-11-05 17:00:48
JS读取本地文件时报错或无响应的常见原因
2026-11-05 16:32:31
let、const与var的区别:JavaScript变量声明最佳实践
2026-11-05 14:16:38
JS中直接定义静态二维数组的字面量写法
2026-11-05 14:16:08
JS 怎么查找字符串中所有匹配项的位置
2026-11-05 14:15:53
相关标签:








