GitHub

Michael Rose

Fun with Array.prototype.push()

July 29th, 2017 by Michael Rose

At work last week, I wrote some JavaScript where I was expecting Array.prototype.push() to return the array back to me. I'm pretty sure I've made this mistake before — but it's not a hard bug to find once you remember that push actually returns the new array length:

x = ['sup', 'dawg']
x.push('swagger') // returns 3
console.log(x) // prints ['sup', 'dawg', 'swagger']

If you want an array back, you just have to use Array.prototype.concat(), although it's important to note that this array is a shallow copy of the one that you call .concat() on:

x = ['sup', 'dawg']
x.concat('swagger') // returns ['sup', 'dawg', 'swagger']
console.log(x) // prints ['sup', 'dawg']

But this got me thinking: why does Array.prototype.push() return the length of the array? I mean, you've got Array.length for a reason. On multiple occasions, I've forgotten what push actually returns to you, so it's obvious that it doesn't make intuitive sense to me why push returns the array length. I can't recall ever actually using the return value of push for anything — I'd just use the length property. So I ventured to the MDN page for push to see if it could shed any light on the reasons for push's API.

I'll save you a click and say that it offers no particular insight into the historical decisions behind the API — just the standard description for the return value:

Return Value: The new length property of the object upon which the method was called.

I read on for a bit and did find some intriguing things in the description section:

push is intentionally generic. This method can be used with call() or apply() on objects resembling arrays.

This part is pretty standard — array methods are generic for things like performing operations on arguments objects. The use-case that is foremost in my mind is writing argsArray = [].slice.call(arguments) to create an array from an arguments object, which was more useful before the ES2015 rest operator became commonplace.

The second part of the description also makes sense:

The push method relies on a length property to determine where to start inserting the given values.

Push uses the array's length to determine where to put the new elements. This fact does let you do some quirky stuff, like:

x = []
x.length = 4
x.push('sup')
console.log(x); // prints [,,,,'sup']

Here, you end up with empty values in your array (not undefined values, but empty ones that methods like map and forEach will skip over), which is a bit strange, but nothing too mind-bending.

The last part of the description is what really intrigued me:

If the length property cannot be converted into a number, the index used is 0. This includes the possibility of length being nonexistent, in which case length will also be created.

At first, this made me wonder if you can have an array with a non-numeric length. So I fired up my browser console and tried:

x = []
x.length = 'sup dawg'
VM139:1 Uncaught RangeError: Invalid array length
    at <anonymous>:1:9

Sweet, no surprise there really. The browser basically says "Why on God's green earth would you do such a thing to a poor wee array? The length property is the only thing keeping it sane!" (Yes, the Chrome developer console is Scottish.)

But then why does the description for Array.prototype.push() talk about non-numeric and non-existent lengths? As it turns out, it has to do with the very first part of the description: push is generic and can operate on array-like objects. And since all arrays are objects, objects are array-like objects. So using objects, you can find out what push does with a non-numeric length value:

x = { length: 'sup dawg', 0: 'brother', 1: 'sister', 2: 'father' }
[].push.call(x, 'mother')
console.log(x) // prints { length: 0, 0: 'mother', 1: 'sister', 2: 'father' }

Say whaaaaaaaat? I had a good laugh at this behaviour, for no particular reason other than that JavaScript and its idiosyncrasies can be funny if you're in the right mood. Yes indeed, push behaves exactly as MDN states: if the length property of the array-like can't be coerced to a number, it uses the index 0 to decide where to put the new element. In the example above, it actually ends up overwriting the value of the 0 property on the x object. Pretty funky stuff.

After reading the push docs a bit more, MDN actually has a more sane example of where you might find the generic behaviour of push useful:

var obj = {
    length: 0,

    addElem: function addElem(elem) {
        // obj.length is automatically incremented
        // every time an element is added.
        [].push.call(this, elem);
    }
};

// Let's add some empty objects just to illustrate.
obj.addElem({});
obj.addElem({});
console.log(obj.length);
// → 2

But I still struggle to think of a practical example where you'd want to do this to an object, instead of just using something like an array member of an abstract data type. What's that old adage? Write code for humans first, computers second. (Although that's usually in reference to performance.)

So we've come all this way through the docs for push, and we've learned some interesting stuff, but we still lack answers to the original question: why does push return the array length? Is there any canonical rationale behind this return value? I'm not about to claim that returning the mutated array is hands-down a better API, and there's no point in getting angry about it either — push's API is here to stay, and we have concat for most use-cases anyway. At this point, I'm just curious about the original design choice.

I'll think about the problem a bit more myself, maybe go digging into the ECMAScript spec, maybe eventually tweet at Brendan Eich or someone else who could shed some light. If I discover any groundbreaking revelations I'll be sure to make a follow up post. Because who doesn't love reading about Array.prototype.push?

EDIT (2017-08-20): Fixed typo: Array.prototype.length -> Array.length. The length property, of course, can't be on the prototype.