Perhaps I could also explain at a higher-level what I'm trying to do.
I'm trying to make a catalog of every Lodash function, and how one might go about doing the same kind of thing without Lodash (and, boy, there's a lot of Lodash functions, it's a little tedious but I'm about half way through). I'm not trying to provide alternatives that have the exact same implementation as the Lodash's functions, it's more like "I'd normally reach for _.x() because of Y - how might I instead do Y without Lodash?" Sometimes the alternative is dead-simple, sometimes it's a fair bit more involved. Right now I'm working on the various _.isWhatever()
functions.
So, for this _.isPlainObject()
function, I'm thinking that a good "vanilla" JS alternative could be something like this:
function isPlainObject(value) {
if (value === null || value === undefined) {
return false;
}
const protoOf = Object.getPrototypeOf;
if (protoOf(value) === null) {
return true;
}
const objectConstructorAsString = 'function Object() { [native code] }';
return (
protoOf(protoOf(value)) === null &&
typeof protoOf(value).constructor === 'function' &&
Function.prototype.toString.call(protoOf(value).constructor) === objectConstructorAsString
);
}
This implementation can be fooled (but as @mhofman mentioned, there's really not a way to get around that today):
// Fooling the isPlainObject() check
class SpecialClass {
static {
Object.setPrototypeOf(SpecialClass.prototype, null)
SpecialClass.prototype.constructor = Object;
}
}
isPlainObject(new SpecialClass()); // true
And, there could be other protections I could layer on there to try and prevent the spoofing, but it's a loosing game that I'm not wanting to dive too deep into :p
Some of the other _.isWhatever()
functions that I'm thinking about are these:
function isMap(value) {
try {
Map.prototype.get.call(value, undefined);
return true;
} catch (error) {
if (error instanceof TypeError) {
return false;
}
throw error;
}
}
function isSet(value) {
try {
Set.prototype.has.call(value, undefined);
return true;
} catch (error) {
if (error instanceof TypeError) {
return false;
}
throw error;
}
}
function isArrayBuffer(value) {
try {
ArrayBuffer.prototype.slice.call(value, 0, 0);
return true;
} catch (error) {
if (error instanceof TypeError) {
return false;
}
throw error;
}
}
// ...similar stuff for isDate, isWeakMap, isWeakSet, isTypedArray, etc
As far as I can tell, none of these can be spoofed (apart from monkey-patching the built-ins), which makes them stronger than Lodash's versions, since Lodash relies on Object.prototype.toString.call()
for each of these. Though, some of the TypeError messages that get thrown worry me a bit. For the Date class, if I do Date.prototype.valueOf.call('this-is-not-a-date')
in node, it gives me the error "this is not a Date object" - that makes me feel like this is pretty good for a date-class brand-check. But with Map.prototype.get.call('this-is-not-a-map', undefined)
I instead get the error "Method Map.prototype.get called on incompatible receiver this-is-not-a-map", which makes it sound like it could potentially support other receivers besides map instances - but looking at the spec, Map.prototype.get.call()
says it only accepts receivers that have the [[mapData]]
internal slot, and the only way to install this internal slot onto an object, as far as I can tell, is by creating a Map
instance - is it possible that platforms are allowed to create their own custom "Map" objects that also have the [[mapData]]
internal slot? Am I reading too much into this error? (The same kind of "weaker error" seems to happen for Sets, typed arrays, WeakMap, and WeakSet)
I also found _.isRegExp()
to be a bit tricky - it seems that every method the language provides for regular expressions is intended to be usable on any regexp-like object - none of them seem to provide brand-checking capabilities. So, the best I could come up with was this:
function isRegExp(value) {
return RegExp(value) === value;
}
...which can be fooled, but seems to have a fair number of checks already built into it (like checking the .constructor
property), so don't know if you can do much better than that.
Checking if something is a boolean object was another hard one - I don't believe there's a reliable cross-realm way to do that. I just resorted to using Object.prototype.toString.call()
, but it feels icky everytime I do that.