先记住这个答案
Map.size 表示当前有多少个键值条目,set 一个新键加 1,delete 成功减 1,更新已有键的值数量不变。给 Map 对象挂的普通属性不算条目,也不计入 size。普通对象没有自动维护的数量属性,常用 Object.keys(obj).length,但它只统计对象自己的、可枚举的、字符串类型的键。要把 Symbol 键和不可枚举属性也算进去,用 Reflect.ownKeys(obj).length。数组的 length 是最大索引加一,稀疏数组里它会大于实际元素个数。
Map.size数条目,给Map挂的普通属性不算Object.keys只数自有、可枚举的字符串键- 数全部自有键用
Reflect.ownKeys - 数组
length是最大索引加一,不等于元素个数
同一组操作,不同的计数结果
下面分别看 Map、带特殊属性的对象和稀疏数组,三种计数方式各自数到了什么。
const map = new Map([['a', 1]])
map.set('a', 2) // 更新已有键
map.note = 'debug' // 挂一个普通属性
console.log(map.size, Object.keys(map).length)
const object = { visible: 1 }
Object.defineProperty(object, 'hidden', { value: 2 }) // 不可枚举
object[Symbol('tag')] = 3 // Symbol 键
console.log(Object.keys(object).length, Reflect.ownKeys(object).length)
console.log(new Array(4).length, Object.keys(new Array(4)).length)查看输出与解释
1 1
1 3
4 0第一行两个 1 来源完全不同。map.size 的 1 是条目 'a',更新值没有让它变成 2;Object.keys(map) 的 1 是 note 这个普通属性,它根本看不到条目。第二行 Object.keys 只数到了 visible,而 Reflect.ownKeys 连不可枚举的 hidden 和 Symbol 键都数到了。第三行是长度为 4 但一个元素都没有的稀疏数组。
普通对象为什么没有 length
length 不是对象的通用能力。数组有 length,是因为数组对它做了特殊处理,添加索引时会自动更新。普通对象读 obj.length,只是在读一个叫 length 的普通属性,没有定义过就是 undefined。
- 选哪种统计方式
- 配置面板显示「共几个字段」,通常只关心用户看得见的字段,
Object.keys就合适。做深度比较、复制工具这类需要完整反射的场景,才需要Reflect.ownKeys。for...in也能数,但它会把原型链上可枚举的属性一起算进去,要配合Object.hasOwn过滤。 另外,Object.keys每次调用都会生成一个新数组。在高频循环里反复统计一个大对象的键数,开销不小。数据本来就是频繁增删的键值集合时,直接用Map,读size不需要额外开销。
容易答错的地方
- 用
obj.length统计属性个数 - 普通对象的
length就是一个普通属性,不会随着添加删除属性自动变化。统计属性个数要用Object.keys或Reflect.ownKeys。 - 给
size赋 0 来清空Map size定义在Map.prototype上,只有读取函数,没有写入函数。非严格模式下map.size = 0静默失败,严格模式下抛TypeError,条目一个都没删。清空用map.clear()。
面试官还会怎么问?
Set 有 size 吗?WeakMap 呢?
Set 有,含义和 Map 一样,是成员个数。WeakMap 和 WeakSet 没有 size,也不能遍历,因为它们的成员可能随时被垃圾回收,给出一个确定的数字就等于暴露了回收时机。
Object.keys 返回的键包含 Symbol 吗?
不包含。只要 Symbol 键可以用 Object.getOwnPropertySymbols,要全部自有键用 Reflect.ownKeys,它会同时返回字符串键和 Symbol 键,包括不可枚举的。
怎么统计数组里真正有多少个元素?
稠密数组直接用 length。可能有空位的数组,用 Object.keys(arr).length 数实际存在的索引,或者 arr.filter(() => true).length,因为 filter 会跳过空位。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。