来聊聊一个在JavaScript开发中很常见的问题:如何判断一个对象自身为空。
你可能觉得这很简单,不就是判断 {} 吗?但实际情况要复杂得多。
什么是“空对象”?
在开始之前,我们需要先明确一下“空对象”的定义。通常,我们说的“空对象”指的是一个不包含任何自身可枚举属性的对象。
注意这里的几个关键词:
- • 自身:指的是对象通过赋值直接拥有的属性,而不是从原型链上继承来的。
- • 可枚举:指的是属性的
enumerable描述符为true的属性。
一个典型的空对象是 {}。但下面这些情况,算不算空对象呢?
let obj1 = {};
let obj2 = Object.create(null); // 一个没有原型的对象
let obj3 = { a: undefined }; // 属性值为undefined
let obj4 = Object.create({ inheritedProp: 'value' }); // 对象自身为空,但原型上有属性
对于日常开发,我们通常关心的是 obj1 和 obj2 这种情况。obj3 虽然属性值是 undefined,但它确实有一个自身属性 a,所以不算空。obj4 自身没有属性,但从原型上继承了一个,这需要根据你的具体需求来判断。
为什么不能直接用 if 判断?
很多新手会尝试用下面的方法:
if (myObject) {
// 对象不为空?
}
或者:
if (myObject == {}) {
// 对象等于空对象?
}
这两种方法都是错误的。
第一种方法只是判断 myObject 是否为“真值”。{}、[]、甚至 new Boolean(false) 在 if 语句中都会被判定为真。它完全不是在检查对象是否为空。
第二种方法试图比较两个对象是否相等。在JavaScript中,对象是引用类型,== 或 === 比较的是它们是否指向内存中的同一个地址。myObject == {} 永远为 false,因为这是两个不同的对象。
所以,我们必须寻找更可靠的方法。
常用方法分析与比较
下面我们来逐一分析几种常见的方法,看看它们的优缺点。
方法一:JSON.stringify
这是网上很流行的一种方法。
function isEmpty(obj) {
return JSON.stringify(obj) === '{}';
}
它的工作原理是: 将对象序列化成JSON字符串,然后和字符串 "{}" 比较。
优点:
- • 写法简单,一目了然。
- • 对于普通的对象字面量,通常有效。
缺点:
- 1. 性能问题:序列化整个对象有一定开销,尤其是对象很大时。
- 2. 功能局限:
-
- • 如果对象属性值为
undefined、function或Symbol类型,它们在序列化时会被忽略。{ a: undefined, b: function(){} }会被判断为空,这可能不是你想要的结果。 - • 如果对象有循环引用(例如
obj.self = obj),JSON.stringify会直接报错。
- • 如果对象属性值为
- 3. 忽略不可枚举属性:它只序列化可枚举的属性。
注意:这个方法判断的是“对象序列化成JSON后是否为空”,而不是“对象自身是否为空”。这两个概念在特定场景下有区别。
方法二:Object.keys
这是目前最推荐、最常用的方法。
function isEmpty(obj) {
return Object.keys(obj).length === 0;
}
它的工作原理是: Object.keys() 方法返回一个由对象自身的、可枚举的属性名组成的数组。我们检查这个数组的长度是否为0。
优点:
- • 语义清晰,直接检查“自身可枚举属性”。
- • 性能很好,避免了不必要的序列化。
- • 是ES5标准方法,兼容性不错。
缺点:
-
• 只检查自身可枚举属性。如果对象有不可枚举的自身属性,或者只有从原型继承的属性,它会被误判为空。
let obj = Object.create(null); Object.defineProperty(obj, 'hidden', { value: 'secret', enumerable: false // 不可枚举 }); console.log(Object.keys(obj).length); // 0 console.log(isEmpty(obj)); // true,但实际上obj有一个属性
对于绝大多数日常场景,Object.keys 方法已经足够好了。它的缺点通常不会造成问题。
方法三:for...in 循环
这是一种比较传统的方法。
function isEmpty(obj) {
for (let key in obj) {
// 如果循环体被执行了,说明至少有一个可枚举属性(包括继承的)
return false;
}
return true;
}
它的工作原理是: for...in 循环会遍历对象所有可枚举的属性,包括从原型链上继承的。只要循环开始,就说明对象有属性。
缺点:
-
• 它会遍历到继承的属性。如果一个空对象是从一个非空原型创建的,它会被误判为非空。
let parent = { inherited: 'value' }; let child = Object.create(parent); console.log(isEmpty(child)); // false,因为遍历到了inherited属性
为了解决这个问题,我们通常需要配合 hasOwnProperty 方法,只检查自身属性:
function isEmpty(obj) {
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
return false;
}
}
return true;
}
这样修改后,它的功能和 Object.keys 方法就基本一致了。但写法更繁琐,性能也可能稍差一些。
方法四:Object.getOwnPropertyNames
如果你想连不可枚举的自身属性也检查,可以使用这个方法。
function isEmpty(obj) {
return Object.getOwnPropertyNames(obj).length === 0;
}
Object.getOwnPropertyNames() 返回对象所有自身属性的名称(包括不可枚举的,但不包括Symbol属性)。
使用场景: 当你需要非常严格地判断一个对象是否“绝对干净”,没有任何自身属性时。例如,在某些库或框架的底层实现中。
对于普通业务代码,很少需要这么严格的检查。
方法五:Reflect.ownKeys
这是ES6引入的最全面的方法。
function isEmpty(obj) {
return Reflect.ownKeys(obj).length === 0;
}
Reflect.ownKeys() 返回一个包含对象所有自身属性键的数组,包括:
- • 字符串类型的可枚举和不可枚举属性
- • Symbol类型的属性
这是目前功能最强大的检查方法,涵盖了所有自身属性的情况。
方法对比总结
为了更直观,我们用一个表格来对比这几种方法:
| 方法 | 检查自身属性? | 检查可枚举属性? | 检查Symbol属性? | 是否遍历原型链? | 推荐指数 |
|---|---|---|---|---|---|
| JSON.stringify | 是 | 仅可枚举 | 否 | 否 | ⭐⭐ |
| Object.keys() | 是 | 仅可枚举 | 否 | 否 | ⭐⭐⭐⭐⭐ |
| for...in | 默认否,需配合hasOwnProperty | 仅可枚举 | 否 | 是 | ⭐⭐ |
| Object.getOwnPropertyNames() | 是 | 可枚举 + 不可枚举 | 否 | 否 | ⭐⭐⭐⭐ |
| Reflect.ownKeys() | 是 | 可枚举 + 不可枚举 | 是 | 否 | ⭐⭐⭐⭐ |
实际应用中的注意事项
知道了方法,我们还要知道怎么用。在实际项目中,你可能会遇到下面这些情况。
情况一:处理 null 和 undefined
上面的所有函数,如果直接传入 null 或 undefined 都会报错。因为我们需要在这些方法上访问属性或调用方法。
一个健壮的实现应该先处理边界情况:
function isEmpty(obj) {
// 检查是否为 null 或 undefined
if (obj == null) {
return true; // 通常认为 null 和 undefined 是“空”的
}
return Object.keys(obj).length === 0;
}
这里用了 == null,它可以同时检查 null 和 undefined。
情况二:处理非对象类型
如果有人不小心传入了字符串、数字或数组呢?
isEmpty(123); // 数字
isEmpty("hello"); // 字符串
isEmpty([]); // 数组
对于数字和字符串,Object.keys() 会先将它们转换为包装对象(new Number(123), new String("hello")),然后返回空数组,导致函数返回 true。这可能不是你想要的行为。
数组比较特殊,Object.keys([]) 返回 [],长度为0,所以也会被判断为空。但很多时候,空数组 [] 和空对象 {} 在我们的逻辑里代表不同的含义。
因此,更严谨的实现可能需要先判断类型:
function isEmpty(obj) {
if (obj == null) {
return true;
}
// 只处理对象类型(包括数组、函数等)
if (typeof obj !== 'object' && typeof obj !== 'function') {
return false; // 或者 throw new Error('Expected an object')
}
return Object.keys(obj).length === 0;
}
是否要检查得这么严格,取决于你的函数设计目标和上下文。
情况三:你真的需要判断“空”吗?
有时候,我们写 isEmpty 函数,背后真正的需求可能是“对象是否有有效的值”。
考虑这个场景:你从表单得到一个对象 userInput,你想知道用户是否填写了任何字段。
let userInput = {
name: '',
age: 0,
email: null
};
如果用我们上面的 isEmpty 函数,它会返回 false,因为对象有三个属性。但这三个属性的值都是“假值”。你可能真正需要的是检查所有属性值是否都为“假”。
function isValuesEmpty(obj) {
for (let key in obj) {
if (obj.hasOwnProperty(key) && obj[key]) {
// 如果有一个属性为真值,就返回false
return false;
}
}
return true;
}
// isValuesEmpty(userInput) 返回 true
所以,在动手写代码之前,先想清楚你的业务需求到底是什么。
希望这篇文章能帮你彻底搞清楚如何在JavaScript中判断空对象。记住,没有一种方法能适用于所有情况,关键是理解每种方法的原理,然后根据你的实际需求来选择。