JS怎么获取普通对象的属性个数
本文详细解答在JavaScript中如何获取普通对象的属性个数。从最常用的Object.keys(obj).length展开,深入探讨不可枚举属性、Symbol键以及原型链干扰的处理方式,并提供完整的流程图和API选型建议。
假设你正在处理接口返回的用户配置对象 const userConfig = { theme: "dark", autoSave: true, fontSize: 14 },想要判断该对象中是否已经配置了属性,或者校验提交表单的字段数量。很多人下意识会尝试写出 userConfig.length,但控制台却输出了 undefined。在 JavaScript 中,普通对象(Plain Object)本身并不内置维护长度信息的 length 或 size 属性。要在实际业务中准确获取普通对象的属性个数,最标准、也是 95% 以上开发场景下的首选写法是:Object.keys(obj).length。
一、为什么不能直接通过 obj.length 获取?
在深入具体方法前,需要厘清 JavaScript 内部的数据结构机制。对于数组(Array)而言,引擎底层维护了一个特殊的自动更新属性 length,每次调用 push()、pop() 或通过数字下标修改数组元素时,数组长度都会实时联动。同样,ES6 引入的 Map 与 Set 也是专职用于集合操作的类,提供了原生的 size 访问器属性。
然而普通对象(通过 {} 字面量或 new Object() 创建的对象)被设计为键值对哈希映射字典。对象的键名是离散且无序的字符串或 Symbol,引擎没有理由为每个对象实时维护一个可能带来额外开销的计数器。因此,读取 obj.length 只会去查找对象上名为 "length" 的属性,如果对象未曾显式定义该字段,自然返回 undefined。
const user = { name: "Alice", age: 25 };
// 示例输出:undefined(普通对象无原生 length 属性)
console.log(user.length);
// 正确做法:先提取属性键名数组,再求其长度
console.log(Object.keys(user).length); // 示例输出:2
二、标准常用解法:Object.keys(obj).length
Object.keys() 是 ECMAScript 5 就已标准化的静态方法,它接收目标对象作为参数,返回一个包含该对象自身所有可枚举(enumerable)字符串属性键名的数组。配合数组自带的 .length 属性,便能直接得出属性个数。
代码示例与常见操作验证
// 1. 空对象
const emptyObj = {};
console.log(Object.keys(emptyObj).length); // 示例输出:0
// 2. 常规数据对象
const product = {
id: "P-1002",
title: "无线键盘",
price: 199,
inStock: true
};
console.log(Object.keys(product).length); // 示例输出:4
// 3. 动态删除属性后
delete product.inStock;
console.log(Object.keys(product).length); // 示例输出:3
Object.keys(obj).length 成为工业界标准方案的核心原因在于其严格的边界控制:
- 仅统计自身属性:不会沿着原型链(Prototype Chain)向上查找,天然隔离继承属性。
- 仅统计可枚举属性:自动过滤 JavaScript 内部机制注入的不可枚举字段。
- 执行预期稳定:只要属性是通过常规字面量或赋值语句创建的,均能被 100% 准确计入。
三、包含特殊属性时的完整处理方案
尽管 Object.keys() 能够应对绝大多数业务需求,但在一些涉及底层框架开发、元编程或对象深度镜像的场景下,对象中可能会引入不可枚举属性或 Symbol 类型键名。此时需要根据目标属性的可见性范围选用更精确的方法。
1. 统计包含不可枚举属性的个数:Object.getOwnPropertyNames()
当通过 Object.defineProperty() 显式指定 enumerable: false 时,该属性虽然真实存在于对象上,但会被 Object.keys() 忽略。如果需要获取包含这类“隐藏属性”在内的所有字符串键个数,应使用 Object.getOwnPropertyNames()。
const systemConfig = {
mode: "production"
};
// 定义一个不可枚举属性
Object.defineProperty(systemConfig, "secretKey", {
value: "auth_token_9981",
enumerable: false // 隐藏,不可枚举
});
console.log(Object.keys(systemConfig).length); // 示例输出:1(忽略 secretKey)
console.log(Object.getOwnPropertyNames(systemConfig).length); // 示例输出:2(包含 secretKey)
2. 统计以 Symbol 作为键名的属性:Object.getOwnPropertySymbols()
ES6 引入的 Symbol 可以作为对象的唯一属性键。不论该 Symbol 属性是否可枚举,Object.keys() 和 Object.getOwnPropertyNames() 都会将其跳过。若要专门统计这类属性的个数,需调用 Object.getOwnPropertySymbols()。
const symId = Symbol("userId");
const symHash = Symbol("userHash");
const userProfile = {
account: "developer",
[symId]: 8801,
[symHash]: "a8f7c9e"
};
console.log(Object.keys(userProfile).length); // 示例输出:1(仅 account)
console.log(Object.getOwnPropertySymbols(userProfile).length); // 示例输出:2(提取两个Symbol键)
3. 统计全部自有属性(全量统计):Reflect.ownKeys()
如果业务目标是统计对象自身拥有的绝对全量属性(包含可枚举与不可枚举字符串属性,以及全部 Symbol 键),ES6 提供的 Reflect.ownKeys(obj) 是最简洁、一步到位的标准 API。它相当于 Object.getOwnPropertyNames() 与 Object.getOwnPropertySymbols() 结果的并集。
const mixedObject = {
regularProp: "normal"
};
Object.defineProperty(mixedObject, "hiddenProp", {
value: 404,
enumerable: false
});
mixedObject[Symbol("meta")] = "metadata";
// 获取绝对全部自身属性键数量
const totalCount = Reflect.ownKeys(mixedObject).length;
console.log(totalCount); // 示例输出:3
四、原型链属性的陷阱与边界防御
在处理对象属性计数时,初学者容易陷入两个经典陷阱:误用 for...in 循环导致原型链属性渗透,以及直接传入 null / undefined 造成运行时崩溃。
1. 为什么坚决不要用未过滤的 for...in 计数?
for...in 循环会递归沿着对象的原型链向上查找,枚举出所有原型上的可枚举属性。如果对象是通过特定原型继承构建的,或者运行环境中有旧库向 Object.prototype 扩展了公共方法,未过滤的循环会导致计数值出现严重偏差。
const parent = { inheritedAttr: "shared" };
const child = Object.create(parent);
child.ownAttr = "mine";
// 错误示范:使用 for...in 累计
let count = 0;
for (let key in child) {
count++;
}
console.log(count); // 示例输出:2(把原型的 inheritedAttr 也算进去了!)
// 正确做法:直接使用 Object.keys
console.log(Object.keys(child).length); // 示例输出:1(安全隔离原型)
for...in 循环,务必在循环体内部使用现代标准的 Object.hasOwn(obj, key)(ES2022)或 Object.prototype.hasOwnProperty.call(obj, key) 严格判断是否为自身属性。
2. 对空值与异常输入的防御性写法
在真实的业务链路中,待统计的对象往往来自函数传参或接口反序列化,其值可能是 null 或 undefined。如果直接执行 Object.keys(null),V8 引擎会立即抛出 TypeError: Cannot convert undefined or null to object,直接中断整个调用栈。
在工程实践中,推荐使用空值合并运算符(Nullish Coalescing Operator ??)进行防御性兜底:
// 高健壮性生产写法:安全返回 0 而不是抛出致命异常
function getObjectSize(target) {
return Object.keys(target ?? {}).length;
}
console.log(getObjectSize({ a: 1, b: 2 })); // 示例输出:2
console.log(getObjectSize(null)); // 示例输出:0
console.log(getObjectSize(undefined)); // 示例输出:0
五、性能考量与高频场景下的数据结构选型
理解 Object.keys(obj).length 的底层成本对于高性能系统尤为关键。该方法的运行流程分为两步:
- 引擎遍历对象并收集所有的自有键,在内存堆区新分配一个字符串数组;
- 读取该临时数组的
length属性,随后该临时数组等待垃圾回收(GC)。
这种机制意味着时间复杂度为 $O(N)$,且伴随一次内存分配。对于普通业务对象(十几个或几十个属性),这种开销微乎其微。但在以下场景下需要警惕:
- 在
requestAnimationFrame动画帧或每秒执行上万次的高频函数内调用; - 对象内存储了数万条以上键值对的超大型字典缓存。
替代方案:重构为 ES6 Map
如果业务架构需要频繁执行键值对添加、删除以及实时读取容量大小,应当使用原生 Map 结构替代普通对象。Map 内部维护了高效的哈希桶与尺寸计数器,其 map.size 属性的读取时间复杂度为恒定的 $O(1)$,完全不需要分配临时数组。
// 普通对象高频计数:每次都有 O(N) 遍历及中间数组创建
const cacheObj = {};
// ... 大量写入
const sizeA = Object.keys(cacheObj).length;
// Map 结构:O(1) 原生读取,零额外内存开销
const cacheMap = new Map();
cacheMap.set("item1", 100);
cacheMap.set("item2", 200);
console.log(cacheMap.size); // 示例输出:2(常数级耗时)
六、方案对比与选型总结
为了便于在日常编码与代码审查中快速决策,以下将各种获取属性个数的方法及其特性梳理成表:
| 方法表达式 | 提取属性范围 | 不可枚举属性 | Symbol 键 | 原型链属性 | 推荐使用场景 |
|---|---|---|---|---|---|
Object.keys(obj).length |
自身可枚举字符串键 | 排除 | 排除 | 排除 | 95% 的日常业务场景首选,结果最符合直觉 |
Object.getOwnPropertyNames(obj).length |
自身所有字符串键 | 包含 | 排除 | 排除 | 排查内部隐藏配置、框架对象属性完整性检测 |
Object.getOwnPropertySymbols(obj).length |
自身所有 Symbol 键 | 包含 | 专职统计 | 排除 | 模块私有元数据标记统计、符号键专用计数 |
Reflect.ownKeys(obj).length |
自身绝对全部属性 | 包含 | 包含 | 排除 | 对象完整序列化、深拷贝属性对齐、底层代理开发 |
map.size |
Map 集合的全部键值对 | 不适用 | 天然支持 | 无污染 | 需高频执行增删与容量查询的大数据量字典场景 |
绝大多数情况直接写
Object.keys(obj).length;若担心空指针报错,加上合并符写成 Object.keys(obj ?? {}).length;若对象使用了 Symbol 或隐藏属性,换用 Reflect.ownKeys(obj).length;高频海量字典计数,换用 Map.prototype.size。
JS语法错误会阻止加载吗?区分SyntaxError与资源加载失败
2026-11-05 16:08:46
JavaScript常见SyntaxError语法错误排查与修复方法
2026-11-05 13:58:44
JS中slice、substring和substr截取字符串的区别
2026-11-05 13:43:47
JS 转 TS 手动迁移最佳实践:从配置 tsconfig 到消除 any 类型
2026-11-05 11:53:32
为什么JS数组push后得到的是数字而不是数组?
2026-11-05 09:59:18








