Map、Set 的区别,以及 Map 和 Object 的区别
Category(分类): JavaScript Status: 已整理(2026)
本文保留原文关于 Set 去重、集合运算、Map 遍历、Map/Set/Object 转换、数组去重和 API 对比的内容,并删除抓取产生的
123456行号、重复章节和失效的so./so搜索链接。重点补充SameValueZero、对象键身份、属性顺序、原型污染、浅拷贝和现代 Set 集合运算方法。
一、Set 是什么
Set 是值的集合,每个值最多出现一次。它可以保存原始值,也可以保存对象引用:
const values = new Set([1, 2, 2, NaN, NaN, -0, 0])
console.log(values.size) // 4:1、2、NaN、0
console.log([...values]) // [1, 2, NaN, 0]
Set 的唯一性使用 SameValueZero 判断:
NaN与NaN视为相同;+0与-0视为相同;- 其他原始值大致遵循
===的相等语义; - 对象、数组和函数按引用身份判断,不做深度比较。
const firstObject = { id: 1 }
const secondObject = { id: 1 }
const set = new Set([firstObject, secondObject, firstObject])
console.log(set.size) // 2:两个对象内容相同但身份不同
console.log(set.has(firstObject)) // true
console.log(set.has({ id: 1 })) // false
Set 的迭代顺序是成功插入的顺序,不是“无序”:
const set = new Set(['red', 'green', 'blue'])
set.add('red') // 已存在,不改变位置
set.delete('green')
set.add('green') // 删除后重新加入,会排到末尾
console.log([...set]) // ['red', 'blue', 'green']
1. Set 常用属性和方法
| API | 作用 |
|---|---|
new Set(iterable) | 创建 Set,可接收可迭代对象 |
set.add(value) | 添加值并返回当前 Set |
set.has(value) | 判断值是否存在 |
set.delete(value) | 删除值并返回是否删除成功 |
set.clear() | 删除所有值 |
set.size | 获取成员数量 |
set.keys() | 返回值迭代器,是 values() 的别名 |
set.values() | 返回值迭代器 |
set.entries() | 返回 [value, value] 迭代器 |
set.forEach(callback, thisArg) | 按插入顺序遍历 |
Set 没有通过索引读取成员的 API,需要迭代或转换为数组:
const set = new Set(['a', 'b'])
console.log(set.keys() === set.values()) // false:是两个迭代器对象
console.log([...set.keys()]) // ['a', 'b']
console.log([...set.entries()]) // [['a', 'a'], ['b', 'b']]
keys() 和 values() 每次调用都会返回新的迭代器;它们不是同一个对象,但产生相同的值序列。
二、Set 的类型转换和集合运算
2.1 数组、字符串与 Set
const array = ['value1', 'value2', 'value3']
const fromArray = new Set(array)
const backToArray = [...fromArray]
const fromString = new Set('hello')
console.log(backToArray) // ['value1', 'value2', 'value3']
console.log([...fromString]) // ['h', 'e', 'l', 'o']
console.log(String(fromArray)) // [object Set]:Set 不会自动变成成员拼接字符串
new Set(value) 要求参数是可迭代对象;普通对象 {} 没有默认迭代器,不能直接传入。
2.2 并集、交集和差集
原文使用展开语法模拟集合运算,仍然适用于所有较早的现代环境:
const a = new Set([1, 2, 3])
const b = new Set([4, 3, 2])
const union = new Set([...a, ...b])
const intersection = new Set([...a].filter(value => b.has(value)))
const difference = new Set([...a].filter(value => !b.has(value)))
console.log([...union]) // [1, 2, 3, 4]
console.log([...intersection]) // [2, 3]
console.log([...difference]) // [1]
现代 ECMAScript 已提供集合组合方法,但需要确认目标浏览器/Node.js 版本:
const a = new Set([1, 2, 3])
const b = new Set([3, 4])
console.log([...a.union(b)]) // [1, 2, 3, 4]
console.log([...a.intersection(b)]) // [3]
console.log([...a.difference(b)]) // [1, 2]
console.log([...a.symmetricDifference(b)]) // [1, 2, 4]
console.log(a.isDisjointFrom(new Set([5]))) // true
console.log(a.isSubsetOf(new Set([1, 2, 3, 4]))) // true
console.log(a.isSupersetOf(new Set([1, 2]))) // true
若项目仍需兼容不支持这些方法的旧运行环境,使用前面的展开语法或提供 polyfill。Set 组合方法还支持满足 size、has()、keys() 的 set-like 对象作为参数,Map 的键集合就属于这一类。
三、Map 是什么
Map 是键值对集合。键和值都可以是任意 JavaScript 值,键在同一个 Map 中唯一:
const map = new Map()
const objectKey = { id: 1 }
map.set('name', 'Ada')
map.set(1, 'number key')
map.set(objectKey, 'object key')
console.log(map.get('name')) // Ada
console.log(map.get(1)) // number key
console.log(map.get(objectKey)) // object key
console.log(map.get({ id: 1 })) // undefined:不是同一个对象
如果对同一个键重复 set,只更新值,不增加条目;更新已有键不会把它移动到末尾:
const map = new Map([
['first', 1],
['second', 2]
])
map.set('first', 100)
console.log([...map.keys()]) // ['first', 'second']
map.delete('first')
map.set('first', 100)
console.log([...map.keys()]) // ['second', 'first']
3.1 Map 常用 API
| API | 作用 |
|---|---|
new Map(iterable) | 创建 Map,输入通常是 [key, value] 对的可迭代对象 |
map.set(key, value) | 写入或更新并返回当前 Map |
map.get(key) | 获取值,找不到时返回 undefined |
map.has(key) | 判断键是否存在 |
map.delete(key) | 删除键并返回是否删除成功 |
map.clear() | 清空所有条目 |
map.size | 获取条目数量 |
map.keys() | 按插入顺序返回键迭代器 |
map.values() | 按插入顺序返回值迭代器 |
map.entries() | 按插入顺序返回 [key, value] 迭代器 |
map.forEach(callback, thisArg) | 遍历 (value, key, map) |
Map 的规范要求实现平均访问时间为集合元素数量的次线性复杂度,但规范不强制必须是哈希表,也不保证在每个场景都比 Object 快。不要只凭结论选择数据结构,应结合键类型、操作模式和实际测量。
3.2 Map 的遍历
const map = new Map([
[0, 'zero'],
[1, 'one']
])
for (const [key, value] of map) {
console.log(`${key} = ${value}`)
}
for (const key of map.keys()) {
console.log(key)
}
for (const value of map.values()) {
console.log(value)
}
for (const [key, value] of map.entries()) {
console.log(key, value)
}
map.forEach((value, key, currentMap) => {
console.log(key, value, currentMap === map)
})
Map.prototype.forEach 的回调参数顺序是 (value, key, map),与 Array.prototype.forEach 的 (value, index, array) 不同。Set 的回调参数虽然是 (value, value, set),第二个参数仍然只是为了与 Map 的回调形状保持兼容。
四、Map 的克隆、合并和转换
4.1 Map 与数组互转
const entries = [
['key1', 'value1'],
['key2', 'value2']
]
const map = new Map(entries)
const array = Array.from(map)
const keys = [...map.keys()]
const values = [...map.values()]
const pairs = [...map.entries()]
console.log(map.get('key1')) // value1
console.log(array) // [['key1', 'value1'], ['key2', 'value2']]
console.log(keys) // ['key1', 'key2']
console.log(values) // ['value1', 'value2']
console.log(pairs) // [['key1', 'value1'], ['key2', 'value2']]
4.2 Map 的克隆是浅拷贝
const original = new Map([
['user', { name: 'Ada' }]
])
const clone = new Map(original)
console.log(original === clone) // false
console.log(original.get('user') === clone.get('user')) // true
new Map(original) 只复制条目和键值引用,不会递归复制键和值。需要深拷贝时,应根据类型选择 structuredClone 或领域级复制方案。
4.3 Map 的合并
后面的重复键覆盖前面的值:
const first = new Map([
[1, 'one'],
[2, 'two'],
[3, 'three']
])
const second = new Map([
[1, 'uno'],
[2, 'dos']
])
const merged = new Map([...first, ...second])
console.log(merged.get(1)) // uno
console.log(merged.get(2)) // dos
console.log(merged.get(3)) // three
4.4 Map 转 Object
当业务明确要求 JSON 对象或普通对象时,可以使用 Object.fromEntries:
const map = new Map([
['yes', true],
['no', false]
])
const object = Object.fromEntries(map)
console.log(object) // { yes: true, no: false }
这不是对任意 Map 的无损转换:对象属性键只能是字符串或 Symbol,数字键会转成字符串,对象键会经过属性键转换并可能发生碰撞:
const map = new Map([
[1, 'number'],
['1', 'string']
])
const object = Object.fromEntries(map)
console.log(object) // { '1': 'string' }:两个键在 Object 中冲突
如果键类型很重要,应保留 Map,或使用键值数组、带类型标签的序列化格式。
4.5 Object 转 Map
const object = { a: 1, b: 2 }
const map = new Map(Object.entries(object))
console.log(map.get('a')) // 1
Object.entries 只返回对象自身的可枚举字符串键属性,不包含继承属性和 Symbol 键。若要处理 Symbol,需要另行调用 Object.getOwnPropertySymbols。
4.6 查询字符串转对象
const params = new URLSearchParams('foo=bar&baz=qux')
const object = Object.fromEntries(params)
console.log(object) // { foo: 'bar', baz: 'qux' }
同名查询参数可能出现多次,Object.fromEntries 会保留最后一个值;需要保留全部值时使用 params.getAll(name) 或直接遍历 URLSearchParams。
4.7 Map 与 JSON
JSON 没有原生 Map 类型。Map 的字符串键可以显式转换成对象:
function mapToObject(map) {
return Object.fromEntries(map)
}
const map = new Map([
['yes', true],
['no', false]
])
const json = JSON.stringify(mapToObject(map))
console.log(json) // {"yes":true,"no":false}
键可能是任意类型时,常见方式是序列化为条目数组:
const map = new Map([
[true, 7],
[{ id: 1 }, ['abc']]
])
const json = JSON.stringify([...map])
console.log(json) // [[true,7],[{"id":1},["abc"]]]
解析回 Map:
const map = new Map(JSON.parse('[["a",1],["b",2]]'))
console.log(map.get('a')) // 1
但 JSON 解析出来的对象键是新对象,不可能恢复原 Map 中的对象引用身份。需要可逆持久化时,应设计明确的 schema,而不是只依靠 JSON.stringify([...map])。
五、Set 与数组去重
5.1 Set 去重:现代首选
function uniqueBySet(array) {
if (!Array.isArray(array)) return array
return [...new Set(array)]
}
console.log(uniqueBySet([1, 2, 2, 3, NaN, NaN])) // [1, 2, 3, NaN]
Set 去重保留第一次出现的顺序,使用 SameValueZero;它不会把结构相同但身份不同的对象合并。
5.2 双重循环和 splice
这是原文的历史方式,原始写法会原地修改输入数组,并且使用 ===,所以不能正确合并 NaN。下面为了避免示例产生隐藏副作用,先复制数组再执行 splice:
function uniqueBySplice(array) {
if (!Array.isArray(array)) return array
const result = [...array]
for (let i = 0; i < result.length; i += 1) {
for (let j = i + 1; j < result.length; j += 1) {
if (result[i] === result[j]) {
result.splice(j, 1)
j -= 1
}
}
}
return result
}
它适合展示算法思想,不适合作为大数组的高效实现。
5.3 indexOf
function uniqueByIndexOf(array) {
if (!Array.isArray(array)) return array
const result = []
for (const value of array) {
if (result.indexOf(value) === -1) result.push(value)
}
return result
}
console.log(uniqueByIndexOf([NaN, NaN])) // [NaN, NaN]:indexOf 找不到 NaN
5.4 includes
includes 使用 SameValueZero,因此能处理 NaN:
function uniqueByIncludes(array) {
if (!Array.isArray(array)) return array
const result = []
for (const value of array) {
if (!result.includes(value)) result.push(value)
}
return result
}
console.log(uniqueByIncludes([NaN, NaN, 1, 1])) // [NaN, 1]
5.5 sort 去重
排序后比较相邻元素可以减少比较次数,但默认 sort() 按字符串顺序排序,并且会修改数组;它也不适合保留原顺序:
function uniqueBySort(array) {
if (!Array.isArray(array)) return array
const sorted = [...array].sort((a, b) => a - b)
const result = []
for (const value of sorted) {
const previous = result.at(-1)
const sameValueZero = value === previous ||
(value !== value && previous !== previous)
if (result.length === 0 || !sameValueZero) {
result.push(value)
}
}
return result
}
console.log(uniqueBySort([10, 2, 2, 1])) // [1, 2, 10]
即使复制数组后排序,结果的顺序语义也已经改变。若要求稳定地保留首次出现顺序,使用 Set 或 Map。
5.6 用普通对象作为哈希表的历史写法
原文使用对象键名不可重复来去重,但普通对象会把键转换成字符串,且可能遇到 __proto__、constructor 和对象键碰撞。若只是字符串键,至少使用 null-prototype 对象和 Object.hasOwn:
function uniqueByNullObject(array) {
if (!Array.isArray(array)) return array
const seen = Object.create(null)
return array.filter(value => {
const key = `${typeof value}:${String(value)}`
if (Object.hasOwn(seen, key)) return false
seen[key] = true
return true
})
}
这个版本仍然不能区分 String({ id: 1 }) 与另一个相同字符串化结果的对象,也不能完整表达 Symbol 和复杂对象身份,因此一般不如 Set/Map。
原文中下面这种写法有两个问题:if (!table[key]) 会把已经存在但值为假值的条目当成不存在,普通对象还会继承原型属性:
// 历史示例:不推荐
function legacyUnique(array) {
const table = {}
const result = []
for (const value of array) {
if (!table[value]) {
table[value] = true
result.push(value)
}
}
return result
}
5.7 用 Map 去重
Map 可以按值身份保存键,适合需要同时记录额外信息的场景:
function uniqueByMap(array) {
if (!Array.isArray(array)) return array
const seen = new Map()
const result = []
for (const value of array) {
if (!seen.has(value)) {
seen.set(value, true)
result.push(value)
}
}
return result
}
只需要唯一值时 Set 更直接;需要把值映射到索引、计数或元数据时 Map 更合适。
六、Object、Map、Set 的区别
6.1 数据模型
- Object:属性键值集合,属性键是 String 或 Symbol;适合表示记录、配置和 JSON 对象;
- Map:任意值到任意值的映射;适合频繁增删、非字符串键、需要明确集合语义的场景;
- Set:唯一值集合;适合成员存在性判断、去重和集合运算。
6.2 对比表
| 对比项 | Object | Map | Set |
|---|---|---|---|
| 存储形式 | 属性键 → 值 | 键 → 值 | 值 |
| 键/值类型 | 键为 String 或 Symbol | 键和值均可为任意值 | 值可为任意值 |
| 唯一性 | 属性键唯一 | 键唯一 | 值唯一 |
| 默认条目 | 普通对象有原型继承属性;null-prototype 对象没有 | 没有用户数据条目 | 没有用户数据条目 |
| 顺序 | 自有属性有规范顺序,但数字索引键会先排序,遍历 API 范围不同 | 按首次插入键的顺序 | 按插入值的顺序 |
| 直接迭代 | 默认不可 for...of | 可 for...of | 可 for...of |
| 数量 | Object.keys(obj).length 等 | map.size | set.size |
| JSON | 原生支持对象 JSON | 需自定义转换 | 需自定义转换 |
| 对象身份 | 对象只能先转换成属性键 | 对象键按引用身份 | 对象值按引用身份 |
| 原型 | 普通对象有 Object.prototype 链 | 实例继承 Map.prototype,但条目不在对象属性上 | 实例继承 Set.prototype,但成员不在对象属性上 |
“Object 没有顺序、Map 有顺序”是入门时期的简化。现代 ECMAScript 已规定对象自有属性的键顺序,但规则比 Map 的插入顺序复杂:整数索引键通常按数值升序,其他字符串键和 Symbol 键按创建顺序;不同反射 API 还会筛选不同的属性集合。若数据本身是有序键值集合,Map 的语义更清晰。
6.3 安全性和原型污染
普通对象接收外部键时要注意原型属性和原型污染:
const unsafe = {}
unsafe['__proto__'] = { polluted: true }
const safeRecord = Object.create(null)
safeRecord['__proto__'] = 'ordinary data'
const map = new Map()
map.set('__proto__', 'ordinary map value')
console.log(Object.hasOwn(safeRecord, '__proto__')) // true
console.log(map.get('__proto__')) // ordinary map value
Object.create(null) 没有 Object.prototype,适合做纯字典,但它也没有 hasOwnProperty 等方法;使用 Object.hasOwn(obj, key) 或 Object.prototype.hasOwnProperty.call。Map 对不可信键天然不会把键当作原型属性,但仍需校验值和业务权限。
6.4 性能选择
Map 和 Set 通常为查找、添加、删除提供平均次线性复杂度;Set 的 has 在大集合中通常比线性扫描数组更合适。Object 在固定结构、JSON 互操作、简单记录和静态配置中更自然。
不要写成绝对结论:
- Map 不保证在所有浏览器、所有操作中都更快;
- Object 也不是“只能用于小数据”;
- 数组适合有序列表和按索引访问;
- 选择应由数据模型、键类型、序列化要求和实测结果决定。
七、Object 常用方法与注意事项
7.1 Object.is
Object.is 与 === 大部分相同,但区分 +0/-0,并把 NaN 与自身视为相等:
console.log(+0 === -0) // true
console.log(Object.is(+0, -0)) // false
console.log(NaN === NaN) // false
console.log(Object.is(NaN, NaN)) // true
7.2 Object.assign
Object.assign 是浅拷贝/合并操作,只复制源对象自身可枚举的 String/Symbol 键属性,并通过普通赋值写入目标对象:
const source = { nested: { value: 1 } }
const copy = Object.assign({}, source)
console.log(copy !== source) // true
console.log(copy.nested === source.nested) // true
它会读取源属性,因此源对象的 getter 可能执行;目标对象已有 setter 时也可能触发 setter。需要完整复制描述符时考虑 Object.getOwnPropertyDescriptors 与 Object.defineProperties,需要结构化数据复制时考虑 structuredClone。
7.3 原型和属性枚举
const object = { a: 1, b: 2 }
console.log(Object.keys(object)) // ['a', 'b']
console.log(Object.values(object)) // [1, 2]
console.log(Object.entries(object)) // [['a', 1], ['b', 2]]
console.log(Object.fromEntries([['a', 1]])) // { a: 1 }
const prototype = { inherited: true }
const child = Object.create(prototype, {
own: { value: true, enumerable: true }
})
console.log(Object.keys(child)) // ['own']
console.log('inherited' in child) // true
console.log(Object.hasOwn(child, 'inherited')) // false
__proto__ 是历史访问器属性,不应作为通用原型操作 API。新代码使用 Object.getPrototypeOf、Object.setPrototypeOf 或创建时使用 Object.create;接收外部键时要防范原型污染。
八、使用场景
8.1 Set
- 数组去重和成员存在性判断;
- 去除字符串重复字符;
- 并集、交集、差集和集合关系判断;
- 跟踪已处理 ID、已访问节点和已选标签;
- 需要唯一值但不需要值到数据的映射。
const visited = new Set()
function visit(id) {
if (visited.has(id)) return false
visited.add(id)
return true
}
8.2 Map
- 对象、函数或其他任意值作为键;
- 请求 ID 到请求状态的映射;
- 频繁增加、删除、查询的键值集合;
- 需要保持键插入顺序;
- 需要把值与计数、缓存、元数据关联。
const counts = new Map()
for (const word of ['js', 'map', 'js']) {
counts.set(word, (counts.get(word) ?? 0) + 1)
}
console.log(counts.get('js')) // 2
8.3 Object
- 表示固定字段的实体或配置;
- 与 JSON、表单、HTTP 数据交互;
- 键确定为 String/Symbol,且对象语义比集合语义更重要;
- 需要原型方法或 class 实例行为。
九、总结
- Set 保存唯一值,Map 保存唯一键到值的映射,Object 保存属性键到值的记录;
- Set 和 Map 使用 SameValueZero,对象键/值按引用身份比较;
- Set、Map 都按插入顺序迭代,Set 的
keys()返回值迭代器; - Map 克隆和 Set/Map 展开都是浅拷贝;
- Map 与 Object 的转换可能丢失非字符串键和对象身份,JSON 不能原生表示 Map/Set;
- Set 是现代数组去重的首选,但
indexOf、includes、排序和对象哈希仍有教学或特殊场景价值; - 不要把 Map 一概说成“性能更好”,也不要把普通 Object 键顺序简单说成“无序”;
- 处理外部键时优先考虑 Map 或 null-prototype 对象,并使用
Object.hasOwn。