hand sketched logo of electrons orbiting a nucleus

TIL: How to check for plain objects

Just show me the code:

function isPlainObjectMy(obj) {
  if (obj === null) return false;

  const prototype = Object.getPrototypeOf(obj);
  return prototype === null || prototype === Object.prototype;
}

Where you might use this

In JavaScript, a ton of stuff is an object!

const arr = [];
typeof arr; // 'object'

const date = new Date();
typeof date; // 'object'

class MyClass {}
const myClass = new MyClass();
typeof myClass; // 'object'

const nullValue = null;
typeof nullValue; // 'object'

const regex = /a/;
typeof regex; // 'object'

So when you are writing a utility that expects an object that is more like a dictionary/record/map-like/has-like, you want to check that the object is a "plain" object.

This isn't iron clad

Ok, so it took me a bit to figure out (1) how to break my code and (2) why Lodash isPlainObject code is written the way it is.

Let's see the example that breaks my code:

const iframe = document.createElement('iframe');
document.body.appendChild(iframe);

const iframeObject = iframe.contentWindow.Object;
const obj = new iframeObject();

isPlainObject(obj); // Output: false (expected: true)

What the heck? The Object in the iframe is not the same as the Object in the main window. So the prototype of obj is not Object.prototype.

How does Lodash handle this

function _isPlainObject(value) {
  if (!isObjectLike(value) || getTag(value) !== '[object Object]') {
    return false;
  }
  if (Object.getPrototypeOf(value) === null) {
    return true;
  }
  let proto = value;
  while (Object.getPrototypeOf(proto) !== null) {
    proto = Object.getPrototypeOf(proto);
  }
  return Object.getPrototypeOf(value) === proto;
}
// where
function isObjectLike(value) {
  return typeof value === 'object' && value !== null;
}
// and
function getTag(value) {
  if (value == null) {
    return value === undefined ? '[object Undefined]' : '[object Null]';
  }
  return Object.prototype.toString.call(value);
}

Look at that weird while loop. Lodash keeps checking up the prototype chain until it finds null or Object.prototype. This is a more robust check than the one I provided.

Lodash isPlainObject works with our example

const iframe = document.createElement('iframe');
document.body.appendChild(iframe);

const iframeObject = iframe.contentWindow.Object;
const obj = new iframeObject();

_.isPlainObject(obj); // Output: true

// let's walk up the prototype chain step by step

Object.getPrototypeOf(obj); // {__defineGetter__: f, __defineSetter__: f, ...}
Object.getPrototypeOf(Object.getPrototypeOf(obj)); // null (yay!)

But why?

JavaScript does equality checks on objects by reference. So when you create an object in the iframe, while its Object.prototype implementation might be exactly the same as the one in the main window, the reference (in memory) is different.

Remembering that checking equality is by reference and not value is important when working with objects in JavaScript. And something even Google's AI doesnt always get right:

WARNING: a == b will return false