/ #JavaScript #tip 

JavaScript “is array” vs object in-depth

Detecting Array vs Object in JavaScript with examples

tl;dr To detect if something is an Array in JavaScript, use Array.isArray(somethingObjectToCheck).

This post is a quarter useful, quarter history lesson, half quirky JavaScript exploration.

Table of contents:

Array.isArray behaviour with different types

Examples for this section at observablehq.com/@hugodf/javascript-array-detection-using-array-isarray, you can play around with the values list if anything is missing (click here to do that).

See below the full table for values for which Array.isArray evaluates to true for. Which is to say Array.isArray works, since it’s only true for Arrays, and it’s true for any Array. No matter how it’s initialised and whether or not it’s been initialised using the local window’s Array or another one’s.

For people who are here to know how to detect if an object is an Array: use Array.isArray. That’s it, off you go and solve problems.

For those who want some extra spicy JavaScript coding and want to understand what makes a JavaScript Array an Array you read on or skip to where, we’re going to re-implement isArray.

Type Value as JSON Array.isArray
Empty Array [] true
Array of Number literal [1,2,3] true
Array of String literal [“a”,“b”,“c”] true
Array (using constructor.from) [null,null,null,null,null,null,null,null,null,null] true
Array with empty values using constructor [null,null,null,null,null,null,null,null,null,null] true
Array with values using constructor [1,2,3] true
Array prototype [] true
Array from other window [1,2,3] true
Empty Object {} false
Object with .length property {“length”:10} false
Object whose __prototype__ is Array {} false
Object {“a”:“2”,“b”:“c”} false
Object with nested Array {“nested”:[“2”,“c”]} false
Empty Set {} false
Set of String {} false
Set of Number {} false
Empty Map {} false
Map with different types {} false
String literal “hello world” false
String Object “hello world” false
Boolean true literal true false
Boolean falst literal false false
Explicity undefined undefined false
Implicit undefined undefined false
null null false
Number (integer) 123 false
Number (float) 1.23 false

What’s so special about Array.isArray?

Also known as the “why don’t we just use instanceof Array?” problem.

When checking for Array instance, Array.isArray is preferred over instanceof because it works through iframes. See the MDN section/example

Or if you prefer the MIT explanation:

[…] by the semantics of instanceof, o instanceof Array works correctly only if o is an array created by that page’s original Array constructor (or, equivalently, by use of an array literal in that page).

See Determining with absolute accuracy whether or not a JavaScript object is an array

In plain English:

In JavaScript, the Array constructor/prototype is not shared across windows/iframes. Therefore instanceof Array works, but only if you’re never going to share Arrays across windows/iframes, since at that point, they won’t have the same constructor/prototype.

Implementation options

There’s a full set of implementation attempts and their tabulated output at https://beta.observablehq.com/@hugodf/array-isarray-implementations.

We take some of the approaches from “Determining with absolute accuracy whether or not a JavaScript object is an array” plus the official MDN polyfill for a spin.

The official polyfill

See the MDN docs or check it out in the Observable notebook:

function isArrayMdnOfficial(objToCheck) {
    return Object.prototype.toString.call(objToCheck) === '[object Array]';
}

This 100% works but feels a bit dirty, the only thing to be weary of is:

Object.prototype.toString and Function.prototype.call not being changed (probably a good assumption but still fragile)”

– J Walden - Determining with absolute accuracy whether or not a JavaScript object is an array

This hack is the only way to do this check properly, as we will see.

The duck-typing approach

The duck-typing approach, if it behaves like an Array, then it’s an Array, ie. see if stuff we could expect on an Array to be there.

This could be pushed to an extreme and check for every Array method.

Positive: anything that behaves like an Array would work.

Negative: anything that behaves like an Array would work.

In Computer Science-speak: anything implementing the JavaScript Array interface (or a subset that we’re actually testing for) would work.

Final negative that means we’re not going to use this: it’s ugly, doesn’t scale… And we’ve got native Array.isArray which pretends like it’s a clean function when actually it just toString-s stuff.

function isArrayLengthAndPushCheck(objToCheck) {
    return Boolean(objToCheck) && objToCheck.length != null && typeof objToCheck.push === 'function';
}

Checking the prototype/constructor/instance type

We get the following two functions.

Check the constructor

function isArrayConstructor(objToCheck) {
    return Boolean(objToCheck) && objToCheck.constructor === Array;
}

Use instanceof

function isArrayInstanceOf(objToCheck) {
    return objToCheck instanceof Array;
}

Here’s the ugly output table for these:

Label Array.isArray isArrayConstructor isArrayInstanceOf
Empty Array true true true
Array of Number literal true true true
Array of String literal true true true
Array (using constructor.from) true true true
Array with empty values using constructor true true true
Array with values using constructor true true true
Array prototype true true false
Array from other window true false false
Object whose __prototype__ is Array false true true
Object with nested Array false false false

These all fail the test when an Array is from another window and Objects masquerading as Arrays. The instanceof approach also fails on the constructor (which is an Array) since it’s not an instance of itself.

That’s why MDN predicates using the polyfill which uses the nasty toString call.

For more JavaScript nasties 🍬 and spicies 🌶:

Subscribe to the Code with Hugo newsletter.

Array vs Object: application

Let’s say we want to measure the depth of we have an object with mixed nested arrays/objects like so:

const obj = {
  myKey: {
    nest: {
      doubleNested: 'value',
      nestedArray: [ { key: 'value' } ]
    }
  }
};

The difficulty lies in detecting whether we should treat the value as an object (dictionary) or as a list.

To break down object vs primitive type detection, it’s a case of typeof obj === 'object', see this quick reminder of types of things:

console.assert(typeof '', 'string');
console.assert(typeof new String(), 'string');
console.assert(typeof 1, 'number');
console.assert(typeof Infinity, 'number');
console.assert(typeof NaN, 'number');
console.assert(typeof undefined, 'undefined');

console.assert(typeof [], 'object');
console.assert(typeof null, 'object');
console.assert(typeof {}, 'object');
console.assert(typeof new Map(), 'object');
console.assert(typeof new Set(), 'object');

Now to separate Objects vs Arrays it’s Array.isArray every day (see above):

// Console.assert flips out again
// even though the assertions hold
console.assert(Array.isArray({}), false);
console.assert(Array.isArray(new Map()), false);
console.assert(Array.isArray(new Set()), false);

console.assert(Array.isArray([]), true);
console.assert(Array.isArray(new Array()), true);

That means we should be able to implement maxDepth as:

function maxDepth(obj, depth = 0) {
  if (typeof obj !== 'object') {
    return depth;
  }
  const [values, depthIncrease] = Array.isArray(obj)
    ? [obj, 0]
    : [Object.values(obj), 1];
  return values.length > 0
    ? Math.max(...values.map(
      value => maxDepth(value, depth + depthIncrease))
    )
    : depth;
}

Some of these fail even though the assertions hold 🙄:

console.assert(maxDepth({}), 0);
console.assert(maxDepth(''), 0);
console.assert(maxDepth([ { one: 'deep' } ]), 1);
console.assert(maxDepth({ one: 'deep' }), 1);
console.assert(maxDepth({ one: [ { two: 'deep' } ] }), 2)
console.assert(maxDepth({ one: { two: 'deep' } }), 2)

There we go: a practical application of Array.isArray

unsplash-logoJOSHUA COLEMAN

Author

Hugo Di Francesco

A developer, working out of London writing CSS, JavaScript and Python.

Subscribe for Enterprise Node.js and JavaScript Guides

Build your web platform with modern Node.js/JavaScript best-practices, tools and patterns