先记住这个答案
Map.size 直接表示集合内部当前键值条目数,更新同一个键的值不会增加数量,删除条目会减少数量;给 Map 对象附加普通属性也不计入 size。普通对象没有自动维护业务属性总数的通用 length,Object.keys(obj).length 只统计自有、可枚举、字符串键。若需要包含 Symbol 或不可枚举属性,要选择对应枚举方法,例如 Reflect.ownKeys 的范围更广。数组 length 又是索引边界,不一定等于存在元素数。不同计数 API 回答不同问题,不能只因为都返回数字就互换。
- Map.size 统计内部条目而非对象附加属性
- Object.keys 只包含自有可枚举字符串键
- 数组长度、对象键数与集合条目数不能混用
更新键与增加属性不会产生同一种计数
示例先更新 Map 的同一个键,数量仍为一,再添加普通 note 属性,size 依然为一。Object.keys(map) 却可以看到 note,因为它枚举的是对象属性,不读取内部条目。
第二部分为普通对象增加不可枚举属性和 Symbol,Object.keys 只保留公开字符串字段,Reflect.ownKeys 则看到全部自有键。哪一个数字正确取决于希望统计的范围。
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;
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两个一来自不同存储空间,不能据此认为 Map 条目就是普通属性。稀疏数组长度四但没有自有索引,进一步说明 length 不是所有容器通用的成员计数。
计数结果要和业务字段定义一致
配置面板通常只关心可枚举业务字段,此时 Object.keys 可能合适;对象内部非枚举元数据和 Symbol 不一定应展示或计入用户字段数量。若需求是完整反射检查,则要采用更宽的范围。
继承属性又是另一层,for...in 可能包含原型链上的可枚举字符串键,不能不加筛选就拿来数自有字段。输入来自不同原型时尤其应明确所有权条件,避免统计结果随原型变化。
频繁计数时考虑分配与维护成本
Object.keys 会创建键数组,反复在热循环中调用可能带来额外遍历和分配。若数据本来适合动态集合且经常增删计数,Map.size 的接口更直接;但是否值得迁移还取决于键类型、序列化和更新方式。
手工维护对象 count 字段也需要让所有增删路径同步,漏一次更新就可能长期漂移。若没有实际热点,清晰的枚举计算通常更容易保证正确,不能为了避免一次小分配引入复杂双写状态。
容易答错的地方
- 对普通对象读 length 当成字段数
- length 只是可能存在的普通属性,除非对象自身定义了特定协议,否则不会随业务键增删自动更新。应选择与所需范围匹配的枚举方法,而不是依赖偶然同名字段。
- 给 Map.size 赋零来清空集合
- size 没有普通写入语义,赋值不会按预期删除条目,并可能在严格模式下失败。清空应使用 clear,删除特定键应使用 delete,再通过 size 与 has 验证实际集合状态。
面试官还会怎么问?
更新一个 Map 键为什么不增加 size?
键已经存在时 set 替换对应值,集合里的键值关系数量没有增加。只有新增不同键才增加条目,判断是否不同仍依赖 Map 的键相等规则,对象键按身份而不是字段内容。
Object.keys 会包含 Symbol 吗?
不会,它只返回自有可枚举字符串键。需要 Symbol 可以使用对应 Symbol 枚举方法,或用 Reflect.ownKeys 获取全部自有键后按描述符筛选,先明确是否也需要不可枚举项。
数组的真实记录数量应该怎样数?
取决于缺失位置是否允许、继承索引是否应计入及每项是否合法。密集受控数组可以直接用 length,稀疏或未知输入则需按明确结构规则检查,不能用一个 API 无条件代表业务记录数。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。