JavaScript 中 Map 与 Object 的区别


一、基本概念

// Object:键值对集合,键只能是字符串或 Symbol
let obj = { name: "张三", age: 25 };
obj["name"];       // "张三"
obj[Symbol("id")]; // 也支持 Symbol 键

// Map:键值对集合,键可以是任意类型
let map = new Map();
map.set("name", "张三");
map.set(25, "年龄");          // 数字键
map.set({ a: 1 }, "对象键");   // 对象键
map.set(function(){}, "函数键"); // 函数键

本质区别:Object 是"属性的无序集合",Map 是"键值对的有序映射"。


二、核心区别对比

维度ObjectMap
键的类型字符串或 Symbol任意类型(对象、函数、基本类型)
键的顺序整数键按升序,其他按插入顺序(ES6+)严格按插入顺序
长度Object.keys(obj).length 手动计算map.size 直接获取
性能大量增删时性能下降频繁增删性能稳定
迭代Object.keys/values/entries原生可迭代(for...of
序列化JSON.stringify 原生支持不支持,需手动转换
原型链有原型,可能继承意外属性无原型,干净无继承
安全性可被原型污染攻击无原型,天然安全

三、逐项详解

3.1 键的类型——最核心的区别

Object 的键会被隐式转为字符串

let obj = {};

obj[1] = "数字1";
obj["1"] = "字符串1";

// 1 被转为 "1",两个赋值指向同一个键
obj;   // { "1": "字符串1" }   ← 覆盖了!

obj[true] = "布尔值";
obj;   // { "1": "字符串1", "true": "布尔值" }
// true 被转为 "true"

obj[{ a: 1 }] = "对象";
obj;   // { "1": "字符串1", "true": "布尔值", "[object Object]": "对象" }
// 对象被转为 "[object Object]"

Map 的键保持原始类型

let map = new Map();

let key1 = 1;
let key2 = "1";

map.set(key1, "数字1");
map.set(key2, "字符串1");

map.get(1);       // "数字1"
map.get("1");     // "字符串1"
// 1 和 "1" 是不同的键

map.size;         // 2

let objKey = { a: 1 };
let objKey2 = { a: 1 };

map.set(objKey, "对象A");
map.set(objKey2, "对象B");

map.get(objKey);    // "对象A"
map.get(objKey2);   // "对象B"
// 两个内容相同的对象是不同的键(引用不同)

Map 用什么判断键相等?—— SameValueZero 算法

SameValueZero:
  - 基本类型:值严格相等(类似 ===,但 NaN === NaN 为 true)
  - 引用类型:同一引用
  - 0 === -0  为 true(与 Object.is 不同)
let map = new Map();

map.set(NaN, "不是数字");
map.get(NaN);    // "不是数字"  ← NaN 等于 NaN

map.set(0, "零");
map.get(-0);     // "零"        ← 0 和 -0 相等

3.2 键的顺序

Object 的键顺序(ES6+ 规范化):

规则:
  1. 整数键(如 "1"、"2"、"10")→ 按数值升序排列
  2. 其他字符串键          → 按插入顺序排列
  3. Symbol 键              → 按插入顺序排列
let obj = {};
obj["b"] = 2;
obj["1"] = 1;
obj["a"] = 3;
obj["2"] = 2;
obj["c"] = 4;

Object.keys(obj);
// ["1", "2", "b", "a", "c"]
// 整数键 "1"、"2" 被提前并按数值升序,其余按插入顺序

Map 的键顺序:始终严格按插入顺序

let map = new Map();
map.set("b", 2);
map.set(1, 1);
map.set("a", 3);
map.set(2, 2);
map.set("c", 4);

[...map.keys()];
// ["b", 1, "a", 2, "c"]
// 严格按插入顺序,没有"整数键提前"的特殊行为

3.3 长度获取
// Object:需要遍历计算
let obj = { a: 1, b: 2, c: 3 };
Object.keys(obj).length;   // 3
Reflect.ownKeys(obj).length; // 3
// 每次调用都要创建临时数组,O(n) 复杂度

// Map:直接读取属性
let map = new Map([["a", 1], ["b", 2], ["c", 3]]);
map.size;   // 3
// O(1) 复杂度

3.4 增删查操作
// ── Object ──
let obj = { a: 1, b: 2 };
obj.c = 3;           // 增
obj.a = 10;           // 改
obj.a;                // 查       → 10
delete obj.b;         // 删
"a" in obj;           // 检查存在 → true(含原型链)
obj.hasOwnProperty("a"); // 检查存在(仅自身)

// 删除不存在的键不报错
delete obj.nonexist;  // true(静默成功)

// ── Map ──
let map = new Map([["a", 1], ["b", 2]]);
map.set("c", 3);      // 增
map.set("a", 10);      // 改
map.get("a");          // 查       → 10
map.delete("b");       // 删       → true
map.has("a");          // 检查存在 → true

// 删除不存在的键返回 false
map.delete("nonexist"); // false(明确告诉你没删到)

区别delete 总返回 truemap.delete() 对不存在的键返回 false,反馈更明确。


3.5 迭代
// ── Object:不可直接迭代 ──
let obj = { a: 1, b: 2, c: 3 };

// 必须用辅助方法
for (let key of Object.keys(obj)) { ... }
for (let [key, val] of Object.entries(obj)) { ... }
Object.values(obj).forEach(val => { ... });

// for...in 会遍历原型链
for (let key in obj) {
    if (obj.hasOwnProperty(key)) { ... }  // 必须手动过滤
}

// ── Map:原生可迭代 ──
let map = new Map([["a", 1], ["b", 2], ["c", 3]]);

for (let [key, val] of map) { ... }       // 直接迭代
for (let key of map.keys()) { ... }       // 只迭代键
for (let val of map.values()) { ... }     // 只迭代值
for (let [key, val] of map.entries()) { ... }

// 可直接使用解构
let entries = [...map];   // [["a",1], ["b",2], ["c",3]]

// Map 没有原型链,for...in 不会遍历到额外属性

3.6 JSON 序列化
// ── Object:原生支持 ──
let obj = { name: "张三", age: 25 };
JSON.stringify(obj);   // '{"name":"张三","age":25}'
JSON.parse('{"name":"张三","age":25}');
// { name: "张三", age: 25 }

// ── Map:不支持 ──
let map = new Map([["name", "张三"], ["age", 25]]);
JSON.stringify(map);   // '{}'  ← 空对象,数据全部丢失!

// 手动转换
// Map → JSON
function mapToJson(map) {
    return JSON.stringify([...map]);
}
mapToJson(map);   // '[["name","张三"],["age",25]]'

// JSON → Map
function jsonToMap(json) {
    return new Map(JSON.parse(json));
}
jsonToMap('[["name","张三"],["age",25]]');  // Map(2) {"name"=>"张三", "age"=>25}

3.7 原型链与安全性
// ── Object:有原型链 ──
let obj = {};
obj.toString;       // [Function: toString]  ← 来自 Object.prototype
"toString" in obj;  // true                  ← 自身没有,但原型上有

// 原型污染风险
let malicious = JSON.parse('{"__proto__": {"isAdmin": true}}');
// 如果不安全地合并,可能污染整个原型链

// 安全检查
{}.hasOwnProperty("toString");  // false(自身没有)
{}.toString();                  // "[object Object]"(但能用)

// ── Map:无原型链 ──
let map = new Map();
map.get("toString");   // undefined   ← 没有原型方法
map.has("toString");   // false

// 天然免疫原型污染

3.8 性能特征
操作ObjectMap
创建极快略慢(需初始化内部结构)
添加键值对极快极快
读取极快(V8 有优化)极快
删除键较慢(可能触发去优化)极快
大量增删性能下降性能稳定
大量数据查找快(引擎优化好)

为什么 Map 在频繁增删场景表现更好?

Object 的内部实现:
  V8 将 Object 的属性存储分为"快属性"和"慢属性"
  - 快属性:线性存储,查找极快,但增删可能需要重排
  - 慢属性:哈希表存储,增删快但查找慢
  - 大量增删会导致 V8 在快/慢模式间切换,性能不稳定

Map 的内部实现:
  始终使用哈希表
  - 增删查找性能稳定
  - 不会出现模式切换问题
  - V8 对 Map 的哈希实现做了专门优化

四、各场景下的选型建议

用 Object 的场景
场景原因
结构固定的配置/选项{ theme: "dark", lang: "zh" },语义更明确
传入函数的选项参数function fetch(options),Object 天然适合
需要序列化/反序列化JSON 原生支持
简单的键值映射(键是字符串)无需 Map 的额外开销
类/构造函数实例class User { constructor() { this.name = ""; } }
与第三方库交互大多数库期望 Object 格式
// 典型应用:配置对象
const config = {
    host: "localhost",
    port: 3000,
    debug: false,
    retries: 3
};

// 典型应用:函数选项
function createDB({ host, port, user, password }) {
    // ...
}
createDB({ host: "localhost", port: 3306 });

// 典型应用:API 响应
// 后端返回 JSON → 前端自动解析为 Object
用 Map 的场景
场景原因
键不是字符串(对象/函数/数字作键)Object 的键会被转为字符串
频繁增删键值对Map 性能稳定
需要知道集合大小map.size O(1)
需要保持插入顺序Map 严格按插入顺序迭代
需要快速迭代原生可迭代
需要避免原型污染Map 没有原型链
// 典型应用:以对象为键的缓存
const cache = new Map();
let element = document.getElementById("app");
let data = computeData(element);
cache.set(element, data);     // 用 DOM 元素作键
cache.get(element);           // 快速查找

// 典型应用:计数/分类
const categories = new Map();
categories.set("electronics", new Set());
categories.set("books", new Set());
categories.get("electronics").add("手机");

// 典型应用:关联数据(键-值都是对象)
const userRoles = new Map();
let alice = { name: "Alice" };
let bob = { name: "Bob" };
userRoles.set(alice, ["admin", "editor"]);
userRoles.set(bob, ["viewer"]);

// 典型应用:字符串与字符串的映射(也适合)
const translations = new Map([
    ["hello", "你好"],
    ["world", "世界"],
    ["bye", "再见"]
]);
translations.get("hello");   // "你好"

五、互转方法

// Object → Map
let obj = { a: 1, b: 2 };
let map = new Map(Object.entries(obj));
// Map(2) {"a" => 1, "b" => 2}

// Map → Object
let map2 = new Map([["a", 1], ["b", 2]]);
let obj2 = Object.fromEntries(map2);
// { a: 1, b: 2 }

// Object.entries / Object.fromEntries 是两者互通的桥梁

六、WeakMap——Map 的特殊变体

特性MapWeakMap
键的类型任意类型只能是对象
引用方式强引用弱引用
可迭代(没有 keys/values/entries/size)
垃圾回收不自动回收键对象无其他引用时自动回收
// WeakMap 的典型用途:给对象附加私有数据
let privateData = new WeakMap();

class User {
    constructor(name, password) {
        this.name = name;
        // 密码存在 WeakMap 中,不暴露在实例上
        privateData.set(this, { password: password });
    }
    checkPassword(input) {
        return privateData.get(this).password === input;
    }
}

let user = new User("alice", "secret123");
user.name;               // "alice"
user.password;            // undefined   ← 外部无法访问
user.checkPassword("123"); // false

user = null;  // user 对象被回收后,WeakMap 中的对应条目自动清除
// 密码数据不会泄漏,也不会造成内存泄漏

七、速查对照表

功能ObjectMap
创建{}new Object()new Map()new Map([[k,v]])
obj.key = valobj[key] = valmap.set(key, val)
obj.keyobj[key]map.get(key)
delete obj[key]map.delete(key)
检查存在key in objobj.hasOwnProperty(key)map.has(key)
清空遍历 delete 或重新赋值map.clear()
获取大小Object.keys(obj).lengthmap.size
获取所有键Object.keys(obj)map.keys()
获取所有值Object.values(obj)map.values()
获取所有条目Object.entries(obj)map.entries()
迭代for...in(含原型链)或 for...of Object.entries()for...of(原生)
JSON 序列化原生支持不支持

八、总结

Object 和 Map 都是键值对集合,但设计目标和适用场景不同。

ObjectMap
定位通用的数据容器,语言基石专门的哈希映射数据结构
字符串和 Symbol任意类型
原型有(可能被污染)无(干净)
JSON原生支持不支持
频繁增删性能不稳定性能稳定

简单原则:键是字符串/Symbol、需要 JSON 交互、结构固定的配置——用 Object。键是任意类型、频繁增删、需要迭代或大小统计——用 Map。

更多推荐