Custom Constructor With An Unknown Number of Arguments
...or "What I What I Learned From An Exercise In Futility, Part 1" - how to enforce the 'new' keyword on a custom constructor when the arguments is of an unknown length.
First, a little background: I was recently asked in a job interview: "What if the Array object didn't exist in JavaScript? How could you create that functionality and how might you test it?". I was asked to write pseudo-code on a whiteboard and to specifically tackle the pop and push methods.
It was a good exercise to be given for the role and an intriguing one - so much so that I found myself tackling it on the train home with the idea that it would either be fun puzzle, a learning experience or both.
A JavaScript array is an object, like any other, it just happens to have attributes which are accessible via a numerical index, so this is an array-like object:
var myArray = {
0: "first entry",
1: "second entry",
2: "third entry"
}
This will behave like an array insofar as:
> myArray[0]
first entry
> myArray[1]
second entry
> myArray[1] = "updated!"
> myArray[1]
updated!
The built-in array type is more than just an object - it has a length property and handy mutator and accessor methods to manipulate the array and it's attributes: pop(), push(), shift(), unshift(), sort() ...etc...
1. The length 'property'
The first problem to solve was discerning the "array entries" as opposed to any other properties of the object. If you create an array object and then give it an attribute, like this:
> myArray = [1, 2, 3, 4];
> myArray.testAttr = "test attribute";
"test attribute"
> myArray.length;
4
Well, in that case the length attribute of the array is still 4 and the methods still only access and mutate the 4 entries created on the first line. testAttr remains an attribute of the object but isn't included as an entry when re request the length property.
Also, here's another problem. Imagine my array like object looks like this:
> myArray[0] = "a string"
> myArray[5] = "another string"
The array length in this case should be 6. Not 2. It's the equivalent to this:
> myArray = ["a string", undefined, undefined, indefined, undefined, "another string"];
So, with these issues in mind, my array-like object with added 'length' function looks like this:
var gsArray = function() {
// always create a new var, even if the 'new' keyword is omitted
if (!(this instanceof gsArray)) {
return new gsArray();
}
return this;
};
gsArray.prototype.length = function() {
var len = 0;
for (var prop in this) {
if (prop == parseInt(prop)) {
len = prop;
}
}
return len;
};
Here, I'm looping through all the object's properties and methods, counting those which have names that are purely digits. I'm also ignoring properties that have leading zeroes. This is a slight departure from the way an array behaves, but it's as good as it gets. Here's how the array behaves:
> myArray = new Array();
[]
> myArray[02] = "test";
"test"
> myArray
[undefined, undefined, "poop"]
So. I referenced this section "The length property" - but I haven't exactly created a length property. In my array-like object length is a method rather than a property. That's because you can't give a JavaScript object a dynamic property that's calculated at the point it's accessed. But the distinction is a fine one, under the bonnet of JavaScript accessing the length property is calling a setter method anyway. In other languages length is generally a method. (In Java, the String object has a length() method while the Array object has a length attribute. Which is weird, no?)
After writing the length() function most of the core methods came pretty quickly, but I left sort() until the end because I sensed it would be the most problematic. I was right - and I'll get to that later, but before that let's revisit my constructor because there's an interesting problem there.
2. The Constructor Function
There were a handful of ways of calling the Array constructor that I wanted to mimic:
arrA = new Array();
// create an empty array
arrB = new Array(4);
// create an array with 4 undefined entries
arrC = new Array('foo', 'bar', 'ken');
// create an array with 3 entries (but it could be more)
So here's what I came up with.
var gsArray = function() {
if (arguments) {
/* single argument that's a number, create that number of undefined entries */
if ((arguments.length === 1) && (typeof arguments[0] === 'number')) {
for (var i=0, lim = arguments[0]; i < lim ; i++) {
this[i] = undefined;
}
}
else {
/* create supplied args as entries */
for (var i=0, lim = arguments.length; i < lim; i++) {
this[i] = arguments[i];
}
}
}
return this;
}
The arguments of a function are available in the scope of that function as a variable named 'arguments'. You'll notice here that 'arguments' is an array! Just one reason this was a pointless exercise in futility!
It's a common pattern to enforce the 'new' keyword if omitted. I've seen handful of techniques here repeated all over the internets. Most deal with known arguments being passed to the constructor...
var Person = function(firstname, surname) {
if (!(this instanceof Person)) {
return new Person(firstname, surname);
}
return this;
};
But what if your constructor has an undetermined number of argumuents? Passing the arguments back into the call creates a problem:
// BAD ANTI-PATTERN!!
var Person = function() {
if (!(this instanceof Person)) {
return new Person(arguments);
}
return this;
}
The problem here is that all the arguments are passed into the constructor as a single variable - that is, an array of all the arguments. Here we would have a new Person object with a list for a firstname and an undefined surname :(
We need some way of flattening this list before calling the function with the new keyword.
I didn't find a handy pattern on teh internets, the solution I came up with was to use the apply method of a function....
var gsArray = function() {
/* always create a new var, even if the 'new' keyword is omitted */
if (!(this instanceof gsArray)) {
return gsArray.apply((new gsArray()), arguments);
}
/* single argument that's a number, create that number of undefined entries */
if ((arguments.length === 1) && (typeof arguments[0] === 'number')) {
for (var i=0, lim = arguments[0]; i < lim ; i++) {
this[i] = undefined;
}
}
else {
for (var i=0, lim = arguments.length; i < lim ; i++) {
this[i] = arguments[i];
}
}
return this;
};
The apply method of a function calls the function with a given this value and the arguments provided as an array - this is where it differs from the call() method which accepts an argument list - which is what is happening in my 'ANTi-PATTERN' code sample.
Sometime soon I'll write up my findings when re-creating the Array.sort() method, because that was another voyage of discovery on the good ship Futility...
If you read this far - thanks for perservering :) you probably ened up here after a google search - I hope I answered your problem. If not, good luck!
Related Links - my code mimicking the array object
Latest Posts
New Launch: Global Radio App
5:35p.m., 3 Sep
The new Global Radio App for iPhone and iPad was released a little over two weeks ago (20 August) and ...How The sort() Method Of An Array Works
1:17p.m., 2 Dec
... or "What I What I Learned from the Exercise In Futility, Part 2". (This follows on from my earlier ...Muppets Birthday Card
5:47p.m., 28 Nov
Emma loves The Muppets. She even has her own Muppet who we call Emma Too and who was born at ...Detecting Online Status In The Browser
11:55a.m., 28 Nov
I was just heading into a meeting when I was asked how our (mostly web-based) iOS application was going to ...