夯实基础!JavaScript如何判断对象自身为空?

0 阅读7分钟

来聊聊一个在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. 1. 性能问题:序列化整个对象有一定开销,尤其是对象很大时。
  2. 2. 功能局限
    • • 如果对象属性值为 undefinedfunction 或 Symbol 类型,它们在序列化时会被忽略。{ a: undefined, b: function(){} } 会被判断为空,这可能不是你想要的结果。
    • • 如果对象有循环引用(例如 obj.self = obj),JSON.stringify 会直接报错。
  3. 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',
      enumerablefalse // 不可枚举
    });
    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中判断空对象。记住,没有一种方法能适用于所有情况,关键是理解每种方法的原理,然后根据你的实际需求来选择。