在 JavaScript 开发中,我们经常需要判断一个对象是否“为空”。通常,我们认为一个对象为空,是指它没有任何可用的数据属性或方法。然而,由于 JavaScript 的继承机制,所有对象都会从其原型链中获得一组默认的方法(例如 toString、hasOwnProperty 等),因此简单地检查对象自身的属性(如使用 Object.keys 或 for…in 循环)并不能满足某些场景下的需求。

本文将探讨如何判断一个对象是否“真正为空”,即不仅自身没有任何属性,还不包含原型链上自定义的(非标准的)数据或方法。

什么叫”空对象”?

首先需要明确”空对象”的定义。一般来说,我们有两种判断标准:

  • 仅判断对象自身(own properties)是否为空
    例如,使用 Object.keys(obj).length === 0for...in 循环(注意,for...in 还会枚举原型链上可枚举的属性)。

  • 判断对象及其原型链上是否没有额外自定义的属性或方法
    这里的”自定义”通常指开发者额外添加的属性或方法,而不是来自 Object.prototype 上的默认方法。

由于所有对象默认都会继承 Object.prototype 上的一些属性(例如 toStringhasOwnProperty 等),因此如果仅看原型链上默认的方法,那么几乎所有对象都不是”空”的。所以在实际判断时,我们通常把默认的 Object.prototype 属性作为”标准”,而判断对象或其原型链上是否存在除这些标准属性之外的自定义内容。

判断对象自身是否为空

最简单的判断方式是检查对象自身是否拥有任何属性:

1
2
3
4
5
6
7
8
9
function isOwnEmpty(obj) {
return Object.keys(obj).length === 0;
}

const a = {};
console.log(isOwnEmpty(a)); // true

a.foo = 'bar';
console.log(isOwnEmpty(a)); // false

这种方法仅判断对象自身的可枚举属性,不考虑原型链。

判断对象及其原型链上是否有自定义属性

假设我们认为对象是”空”的,当且仅当:

  • 对象自身没有任何属性(包括可枚举和不可枚举属性),
  • 对象原型链上除去 Object.prototype 默认的属性外,也没有其他自定义属性或方法。

一般来说,Object.prototype 默认包含以下常用属性和方法(具体取决于浏览器,但通常包括):

  • constructor
  • toString
  • hasOwnProperty
  • isPrototypeOf
  • propertyIsEnumerable
  • toLocaleString
  • valueOf

我们可以把这些属性作为判断的基线。

下面是一种实现思路,通过遍历对象自身和它的原型链,检查每一层是否包含非默认属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
function isReallyEmpty(obj) {
// 获取 Object.prototype 上的属性名称(包括不可枚举属性)
const defaultProps = Object.getOwnPropertyNames(Object.prototype);

// 检查对象自身的属性
const ownProps = Object.getOwnPropertyNames(obj);
if (ownProps.length > 0) {
return false;
}

// 遍历原型链(直到 Object.prototype 为止)
let proto = Object.getPrototypeOf(obj);
while (proto && proto !== Object.prototype) {
const protoProps = Object.getOwnPropertyNames(proto);
// 如果发现原型上存在非默认的属性,则认为对象不为空
for (let prop of protoProps) {
if (!defaultProps.includes(prop)) {
return false;
}
}
proto = Object.getPrototypeOf(proto);
}
return true;
}

// 测试
const obj1 = Object.create(null); // 没有原型
console.log(isReallyEmpty(obj1)); // true

const obj2 = {}; // 继承自 Object.prototype
console.log(isReallyEmpty(obj2)); // true

// 添加自定义属性到对象自身
obj2.custom = 'value';
console.log(isReallyEmpty(obj2)); // false

// 删除自定义属性
delete obj2.custom;

// 添加自定义属性到原型链上(非 Object.prototype 默认属性)
const proto = { customMethod: function() {} };
const obj3 = Object.create(proto);
console.log(isReallyEmpty(obj3)); // false,因为 proto 中有 customMethod

说明:

  • 我们首先检查对象自身(通过 Object.getOwnPropertyNames)是否有任何属性;
  • 然后遍历对象的原型链(排除最后一级的 Object.prototype),对每一层调用 Object.getOwnPropertyNames,判断是否存在除默认属性外的其他属性;
  • 如果都没有发现,则认为对象是真正”空”的。

注意事项:

  • 如果对象的原型链中存在自定义的但不可枚举的属性,该方法依然可以检测到,因为 Object.getOwnPropertyNames 会返回所有属性,而不仅仅是可枚举属性。
  • 如果只需要判断对象自身是否为空,使用 Object.keysfor...in 循环就足够;而这里的场景要求包含原型链上的自定义数据或方法,因此需要更全面的检测。

希望这篇文章可以帮到你!

Happy Coding!