I have a method in my personal library that is roughly equivalent to:
Object.hasNoKey = function (o) {
return Object.keys(o).length === 0
}
The name is carefully chosen in order to make clear what it really means. (It is almost equivalent to jQuery’s isEmptyObject(), except maybe wrt inherited properties.)
The problem with a “generic” isEmpty() method as defined by Lodash, is that determining the emptiness of an object requires knowledge of its structure, so that it cannot be really generic; e.g., in order to have:
it must be known that new Set(...).size ought to be checked.
you are totally right! But I think there are some ways to fix that, the proposal is very similar to Object.entries implementation.
Object.isEmpty ( O )
When the entries function is called with argument O, the following steps are taken:
Let obj be ? ToObject(O).
Let nameList be ? EnumerableOwnPropertyNames(obj, key+value).
Let arr to be CreateArrayFromList(nameList).
Let size be the length of arr
If size = 0, then
Return true
Else
Return false
(notice the above pseudocode wont work for numbers, or booleans)
No, it returns an empty array with an additional property on it. Object.keys(x).length would be 1, but x.length would be zero.
What about Object.create({ a: 1, b: 2 })? That has zero own keys (so an implementation that counted own keys would say it's empty) but two inherited enumerable ones. If you count inherited and non-enumerable keys, then {} has 12 in my current browser. Do you count Symbol properties too, or only string properties? What about a Map or a Set that's empty, but has extra own enumerable properties?
The question of "what does empty mean" is a very complex one that has a great many potentially valid interpretations. Which one would you suggest the language enshrine, and what about the valid use cases for alternate interpretations?
First of all thx for the feedback, you just blow my mind.
what does empty mean?
Thats the point! I think we need to reach a consensus there. My current answer would be: something like:
function isEmpty(obj) {
if (typeof obj == 'boolean') {
return false;
}
if (obj == undefined || obj == null) {
return true;
}
if (typeof obj == 'number') {
return false;
}
if (obj == '') {
return true;
}
if (obj.constructor == Set) {
return obj.size == 0;
}
if (obj.constructor == Map) {
return obj.size == 0;
}
return Object.entries(obj).length == 0;
}
(As far as I know we cannot tell anything about WeakMap or WeakSet so isEmpty() will fail/give a wrong answer for that )
Does it make sense to you to rely only on own keys? I think it could make sense, at least for me since most of the time I'd use this feature to handle data from server responses.
Regarding symbols I don't have enough knowledge to give you a strong answer but I'd love to know your opinion.
+0, -0, NaN are not empty? What about negative numbers?
I'd expect false to be empty but true not to be.
For Set and Map, we'd need to use a robust brand check and not check the constructor (in the absence of a new protocol), what about subclasses that define their own size getter semantics?
What about a regex? They have no own enumerable keys, so your algorithm would mark them as empty, but i'd consider an empty regex to be one that can't match anything (like RegExp.prototype).
What about a Promise? I'd consider an empty Promise to be one that can't ever resolve, but that's not knowable.
I think that it's entirely possible to bikeshed answers to all of these questions, but I don't know how valuable it would be, since each user/application is likely to have its own concept of "emptiness" combined with "what kinds of objects they want to consider the emptiness of".
I'd prefer this kind of semantics for an Object.isEmpty - it's simple and everyone interprets it as that low-level primitive. It also fits in with the rest:
Object.keys(new Map([["a", 1], ["b", 2]])) returns [], not ["a", "b"].
Object.values(new Map([["a", 1], ["b", 2]])) returns [], not [1, 2].
Better might be just Object.size(o), which returns effectively Object.keys(o).length. I've personally have had a couple cases where the length was useful independent of it, and it's easy enough to check Object.size(o) === 0 or !Object.size(o) for Object.isEmpty(o). And in nearly all runtimes, this is a very cheap O(1) access for non-proxies, cheaper than even property access.
Yes, maybe that kind of semantic is better. As ljharb said the core problem here is each use case will be potentially different and "being emtpy" is a vague concept.
This would be my exagerated use case which I think it's more or less common:
const response = parseServerResponse(rawResponse);
if(isEmpty(response)) {
// Response could be "false", an emtpy array or an emtpy object...
}
// Handle non emtpy responses
IMO most common use case is checking for {} this can be a nightmare for new javascript developers!
Maybe a Object.isVoid method to check for falsy values, empty arrays and of course empty objects would make sense but after ljharb comments I think this is going to be very hard...
It's possible to resolve this by just communicating what the intent is. Nobody does Object.keys(new Map([[1, "1"]])) expecting it to return [1], for instance. So to me personally, that concern of Object.isEmpty(new Set([1])) returning true (as Object.keys(new Set([1])).length is 0) is overblown.
I'm just a js developer, not a js expert at all, but I think this proposal cover most of "common" cases an average developer will encounter in their day to day.
Pseudocode/Polyfill:
The idea would be a recursive approach iterating over all the enumerable properties of the object.
If at some point length or size are defined this properties will be used to determine if the object is empty otherwise
function isEmpty(object) {
if (object === undefined) {
return true;
}
if (object === null) {
return true;
}
if ((typeof object.length) === 'number') {
return object.length === 0;
}
if ((typeof object.size) === 'number') {
return object.size === 0;
}
if (Object.getPrototypeOf(object) === Object.prototype) {
return Object.keys(object).length === 0;
}
return isEmpty(Object.getPrototypeOf(object));
}
With that we cover the following cases with a behaviour that might be suitable for common use cases:
describe('.isEmpty()', () => {
test('Basic types', () => {
expect(isEmpty(-0)).toBe(true);
expect(isEmpty(+0)).toBe(true);
expect(isEmpty(NaN)).toBe(true);
expect(isEmpty(undefined)).toBe(true);
expect(isEmpty(null)).toBe(true);
expect(isEmpty('')).toBe(true);
expect(isEmpty({})).toBe(true); // This is the keypoint of all of this!
expect(isEmpty([])).toBe(true);
expect(isEmpty(true)).toBe(true);
expect(isEmpty(false)).toBe(true);
// Numbers are empty
expect(isEmpty(1)).toBe(true);
expect(isEmpty(0)).toBe(true);
expect(isEmpty(1)).toBe(true);
expect(isEmpty(0x10)).toBe(true);
// Strings rely on its length
expect(isEmpty('')).toBe(true);
expect(isEmpty('0')).toBe(false);
expect(isEmpty('foo')).toBe(false);
});
test('Complex objects', () => {
// "Complex" Objects like arrays could be considered empty when its length === 0
// Array uses array.length
expect(isEmpty(new Array())).toBe(true);
expect(isEmpty(new Float32Array())).toBe(true);
expect(isEmpty([0])).toBe(false);
expect(isEmpty([1])).toBe(false);
// // Set relies on size
expect(isEmpty(new Set())).toBe(true);
expect(isEmpty(new Set(['0']))).toBe(false);
expect(isEmpty(new Set([1]))).toBe(false);
// // Map relies on size
const map = new Map();
expect(isEmpty(map)).toBe(true);
map.set('foo', 'bar');
expect(isEmpty(map))
});
test('Prototypes and stuff', () => {
expect(isEmpty({ a: 1 })).toBe(false);
expect(isEmpty(Object.create({ a: 1, b: 2 }))).toBe(false);
// length takes precedence
expect(isEmpty(Object.assign([], { a: true }))).toBe(true); // (?)
});
test('Non intuitive cases', () => {
// Weakmap is always empty (?)
const weakmap = new WeakMap();
expect(isEmpty(weakmap)).toBe(true);
weakmap.set({}, {});
expect(isEmpty(weakmap)).toBe(true); // (?)
// Weakset is always empty (?)
const weakSet = new WeakSet();
expect(isEmpty(weakSet)).toBe(true);
weakSet.add({});
expect(isEmpty(weakSet)).toBe(true); // (?)
// Dates are empty (?)
expect(isEmpty(new Date())).toBe(true); // (?)
expect(isEmpty(Date.UTC(2020, 20, 10))).toBe(true); // (?)
});
test('Size and length properties will take priority', () => {
class EmptyWeakMap extends WeakMap {
get length() {
return 0;
}
}
const emptyWeakMap = new EmptyWeakMap();
expect(isEmpty(emptyWeakMap)).toBe(true);
class NonEmptyWeakMap extends WeakMap {
get length() {
return 10;
}
}
const nonEmptyWeakMap = new NonEmptyWeakMap();
expect(isEmpty(nonEmptyWeakMap)).toBe(false);
// Same for prototypes
expect(isEmpty(Object.create({ size: 0, b: 2 }))).toBe(true);
expect(isEmpty(Object.create({ length: 0, b: 2 }))).toBe(true);
});
});
with that said I think there should be big community consensus about the expected behavior of this feature.