Extending JavaScript Arrays While Keeping Native Bracket-Notation Functionality

Posted December 1, 2011 at 10:51 AM by Ben Nadel

Tags: Javascript / DHTML

In JavaScript, we can sub-class native data types by extending the native prototypes. This works perfectly with the native String object; but, when it comes to native Arrays, things don't work quite so nicely. If we extend the Array prototype, we inherit the native array functions; but, we no longer have the ability to use bracket notation to set and get indexed values within the given array. Sure, we can use push() and pop() to overcome this limitation; but, if we want to keep the bracket notation feature functional, we have to build on top of an existing array instance rather than truly sub-classing the Array object.


 
 
 

 
  
 
 
 

Typically, when building a sub-class in JavaScript, you extend the super class' prototype and then define your sub-class class methods. When your sub-class is then instantiated, your new object automatically gets all of the functionality defined in the prototype, the super-class prototype, and the rest of the prototype chain. When it comes to "sub-classing" an Array, however, we can't quite use such an elegant approach; instead of putting our sub-class methods in the prototype chain, we have to inject the sub-class methods into the object as part of the instantiation process.

To experiment with this approach, I wanted to create a sub-class of the native JavaScript array called a Collection. The Collection would have all the features of a normal array, plus some utility features. And, this would all be done without modifying the native Array prototype.

collection.js (Our Array Sub-Class)

  • // Define the collection class.
  • window.Collection = (function(){
  •  
  •  
  • // I am the constructor function.
  • function Collection(){
  •  
  • // When creating the collection, we are going to work off
  • // the core array. In order to maintain all of the native
  • // array features, we need to build off a native array.
  • var collection = Object.create( Array.prototype );
  •  
  • // Initialize the array. This line is more complicated than
  • // it needs to be, but I'm trying to keep the approach
  • // generic for learning purposes.
  • collection = (Array.apply( collection, arguments ) || collection);
  •  
  • // Add all the class methods to the collection.
  • Collection.injectClassMethods( collection );
  •  
  • // Return the new collection object.
  • return( collection );
  •  
  • }
  •  
  •  
  • // ------------------------------------------------------ //
  • // ------------------------------------------------------ //
  •  
  •  
  • // Define the static methods.
  • Collection.injectClassMethods = function( collection ){
  •  
  • // Loop over all the prototype methods and add them
  • // to the new collection.
  • for (var method in Collection.prototype){
  •  
  • // Make sure this is a local method.
  • if (Collection.prototype.hasOwnProperty( method )){
  •  
  • // Add the method to the collection.
  • collection[ method ] = Collection.prototype[ method ];
  •  
  • }
  •  
  • }
  •  
  • // Return the updated collection.
  • return( collection );
  •  
  • };
  •  
  •  
  • // I create a new collection from the given array.
  • Collection.fromArray = function( array ){
  •  
  • // Create a new collection.
  • var collection = Collection.apply( null, array );
  •  
  • // Return the new collection.
  • return( collection );
  •  
  • };
  •  
  •  
  • // I determine if the given object is an array.
  • Collection.isArray = function( value ){
  •  
  • // Get it's stringified version.
  • var stringValue = Object.prototype.toString.call( value );
  •  
  • // Check to see if the string represtnation denotes array.
  • return( stringValue.toLowerCase() === "[object array]" );
  •  
  • };
  •  
  •  
  • // ------------------------------------------------------ //
  • // ------------------------------------------------------ //
  •  
  •  
  • // Define the class methods.
  • Collection.prototype = {
  •  
  • // I add the given item to the collection. If the given item
  • // is an array, then each item within the array is added
  • // individually.
  • add: function( value ){
  •  
  • // Check to see if the item is an array.
  • if (Collection.isArray( value )){
  •  
  • // Add each item in the array.
  • for (var i = 0 ; i < value.length ; i++){
  •  
  • // Add the sub-item using default push() method.
  • Array.prototype.push.call( this, value[ i ] );
  •  
  • }
  •  
  • } else {
  •  
  • // Use the default push() method.
  • Array.prototype.push.call( this, value );
  •  
  • }
  •  
  • // Return this object reference for method chaining.
  • return( this );
  •  
  • },
  •  
  •  
  • // I add all the given items to the collection.
  • addAll: function(){
  •  
  • // Loop over all the arguments to add them to the
  • // collection individually.
  • for (var i = 0 ; i < arguments.length ; i++){
  •  
  • // Add the given value.
  • this.add( arguments[ i ] );
  •  
  • }
  •  
  • // Return this object reference for method chaining.
  • return( this );
  •  
  • }
  •  
  • };
  •  
  •  
  • // ------------------------------------------------------ //
  • // ------------------------------------------------------ //
  • // ------------------------------------------------------ //
  • // ------------------------------------------------------ //
  •  
  •  
  • // Return the collection constructor.
  • return( Collection );
  •  
  •  
  • }).call( {} );

As part of this create-and-build workflow, we have to manually instantiate a new array. And, since the JavaScript array can take constructor arguments, we have to use the apply() method to proxy the native constructor. As I talked about yesterday, using apply() to invoke a native Array constructor has some issues of its own. Once we have our new array instance, however, we simply need to append our sub-class properties (including methods) and return the synthesized object.

In this case, we are adding the following methods to our JavaScript Array sub-class:

  • add( value | array )
  • addAll( value1, value2, ... valueN )

Now that we have our Collection class defined, let's use it to see if we can populate and reference values using the native bracket notation.

  • <!DOCTYPE html>
  • <html>
  • <head>
  • <title>Extending JavaScript Arrays And Keeping Native Features</title>
  •  
  • <!-- Include the Collection class. -->
  • <script type="text/javascript" src="./collection.js"></script>
  • <script type="text/javascript">
  •  
  •  
  • // Create a new collection with default values.
  • var friends = new Collection( "Sarah" );
  •  
  •  
  • // Use collection-based API to populate.
  • friends.addAll( "Tricia", "Joanna" );
  •  
  • // Use native array functionality to populate.
  • friends[ 3 ] = "Kit";
  • friends[ 4 ] = "Anna";
  •  
  • // Use native array functionality to remove.
  • friends.splice( 0, 1 );
  •  
  •  
  • // Log the current friends collection.
  • console.log( friends );
  • console.log( "Length:", friends.length );
  •  
  •  
  • </script>
  • </head>
  • <body>
  • <!-- Left intentionally blank. -->
  • </body>
  • </html>

As you can see, we instantiate our Collection with a constructor argument. Then we use both the sub-class property methods as well as the native bracket-notation methods to populate the object. And, when we log out the resultant array sub-class, we get the following console output:

["Tricia", "Joanna", "Kit", "Anna"]
Length: 4

As you can see, not only did we keep the use of bracket notation to update the Array sub-class, the length property was maintained as well. Of course, this isn't surprising since we aren't technically using a sub-class - we're using an array instance. It's just that we've added our sub-class features to the existing array instance.

With any programming language, it is often the small features that make the language enjoyable to use. When it comes to Arrays in JavaScript, there's no doubt that being able to use bracket notation is one of those features. And, in order to sub-class a native JavaScript Array while keeping this feature in-tact, we have to use some smoke and mirrors.




Reader Comments

Dec 1, 2011 at 1:21 PM // reply »
13 Comments

Array.apply looks like a nice way to clone an array (its children still only being copied by reference). myArr.concat() and myArry.slice() works as well.

I've had luck sub-classing Array by simply assigning myConstructor.prototype = []; and this.length inside myConstructor.

IE7 doesn't set the length property correctly but you can test for it with something like:

  •  
  • function Catalog () {
  • this.length = 0;
  • }
  •  
  • Catalog.prototype = [];
  •  
  • var cat = new Catalog();
  •  
  • cat.push(1);
  •  
  • if ( !cat.length ) {
  •  
  • Catalog.prototype = {};
  •  
  • var arrayMethods = [
  • "join",
  • "pop",
  • "push",
  • "reverse",
  • "shift",
  • "slice",
  • "sort",
  • "splice",
  • "unshift"];
  •  
  • for ( var i = 0, max = arrayMethods.length; i < max; i++ ) {
  • Catalog.prototype[ arrayMethods[i] ] = Array.prototype[ arrayMethods[i] ];
  • }
  •  
  • }
  •  
  • // Fix to string Array.prototype.toString errors
  • Catalog.prototype.toString = Object.prototype.toString;
  •  
  • // Fix concat
  • Catalog.prototype.concat = function () {
  • return Array.prototype.concat.apply( this.slice(), arguments );
  • };


Dec 1, 2011 at 3:04 PM // reply »
170 Comments

@Ben:

If you were actually trying to do something like this for real code, I wouldn't bother defining things in the Collection.prototype--I'd just immediately extend the Array object you create in the Collection() constructor.

Since you're not actually returning an instance of the Collection, adding the methods to the Collection doesn't gain you anything and the looping in the injectClassMethods() just slows things down.

I'd instead, just do:

collection = (Array.apply( collection, arguments ) || collection);

collection.add = function (){};
collection.addAll = function ();

The end result is the same, but you've removed some of the unneeded processing.


Dec 1, 2011 at 6:47 PM // reply »
11,238 Comments

@Bob,

In my experimentation, at least on Firefox, even when you set the prototype of the sub-class to be the array literal, "[]", you still lose the ability to use bracket-notation for setting values into the array. Building on top of an array instance has been the only way I could figure out how to keep the use of something like:

  • myArray[ 1 ] = someValue;

It's like bracket notation is some magical part of the language :(

As far as using Array.apply() as a means to duplicate a top-level array, I totally hadn't thought of that - awesome insight!

@Dan,

Using "Collection.prototype", I was trying to accomplish two things:

1) Strictly Emotional: I wanted to give the impression of actually sub-classing an object. As such, I wanted to pretend to use the class prototype as a way to define the class methods.

2) Function References: Assuming that Collection() could create multiple instances (as any constructor function would), I wanted to simply copy function references into the new collection, rather than redefining the methods for each collection instance.

That said, if I only wanted to create a one-off Collection situation, I definitely agree that "inject" approach would do little more than add unnecessary overhead.


Dec 5, 2011 at 11:54 AM // reply »
13 Comments

@Ben,

I tried the Catalog example in FF from my previous post and it seemed to work fine at first.

  • var foo = new Catalog();
  • foo.push( "a" ); // ["a"]
  • foo[0] = 1; // [1]
  • foo.length === 1; // true

But then I dug a little deeper to try and get at the problem you were seeing and I realized my approach comes with a caveat that I hadn't realized before. The Catalog class can only use direct assignment with existing indexes because the length property is no longer maintained properly when adding new indexes directly and in some cases (like adding to an empty array in FF) the assignment itself fails.

  • var foo = new Catalog();
  • foo[0] = "a"; // [] in FF, ["a"] in Chrome
  • foo.length === 1; // false

I guess I've never noticed this behavior because adding to an array using push feels better to me than assigning it directly.

Thanks for pointing this out. It is good to know the the limitations of the prototype = [] solution.


Dec 8, 2011 at 7:42 PM // reply »
11,238 Comments

@Bob,

I'll side with you that perhaps using push/pop feels a bit more natural. But, I definitely use direct index assignment a good deal. This is especially true if I need to "param" an array value... though, granted, I think I do that more on the server-side (in ColdFusion) than I do in JavaScript.

I also didn't realize that there were differences between FF and Chrome on this issue. I typically do all my R&D in Firefox since I *still* find Firebug to be a much better experience than any of the other browser's built-in debug tools and JavaScript consoles. People tell me that the Chrome tools are getting better every day. Perhaps I will start trying to do some R&D in Chrome to get used to them.


Jan 29, 2012 at 11:59 PM // reply »
1 Comments

I like the idea of extending Array and Object, but I've found that doing so can have unexpected results. For example, extending Array breaks attribute handling in SVGElement in Webkit.


Post A Comment

Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.

Please review the following issues:

Author Name:


Author Email:

Author Website:

Comment:

Supported HTML tags for formatting: <strong>bold</strong>   <em>italic</em>   <code>code</code>







  • Help Wanted - Find Your Next ColdFusion Job
Ben Nadel's Company - Epicenter Consulting Recent Blog Comments
May 21, 2013 at 11:51 AM
Ask Ben: Parsing Very Large XML Documents In ColdFusion
Looking at my first ever XML document that I have to parse and put into MS SQL 2000 with CF8. I get it to list the desired Field name, many times over, and have a long list of this field name displa ... read »
May 21, 2013 at 9:25 AM
Turning Off and On Identity Column in SQL Server
you are awesome..i am lucky to get this blog between such a garbage one....Thanks, Prashant ... read »
May 20, 2013 at 4:38 PM
Using A Dynamic Column Name With ValueList() In ColdFusion
@Dana, Your confusion is well founded, since this is a very confusing features. In fact, it ONLY works if you use array notation. Meaning, that this: arrayToList( query[ "columnName" ] ) ... read »
May 20, 2013 at 4:34 PM
Using A Dynamic Column Name With ValueList() In ColdFusion
I was thinking chicken and the egg, I wouldn't have expected it to work in the valuelist going in I guess. Maybe I just need a beer, long day :) ... read »
May 20, 2013 at 4:29 PM
Using A Dynamic Column Name With ValueList() In ColdFusion
@Dana, That's if you're trying to reference a specific row. In this case, we're trying to reference the entire query column as one cohesive value. So, you are correct that if you wanted to output a ... read »
May 20, 2013 at 4:24 PM
Using A Dynamic Column Name With ValueList() In ColdFusion
I thought when you used array notation to reference queries you always had to have the row or it would throw a similar error as well? ... read »
May 20, 2013 at 11:45 AM
Using jQuery's Animate() Step Callback Function To Create Custom Animations
This is really useful. I found out that you don't actually have to use a dummy css property (surprisingly). To animate a property in a linear-gradient for instance I did this this.css('someLinearGra ... read »
May 20, 2013 at 10:51 AM
Using A Dynamic Column Name With ValueList() In ColdFusion
@Josh, Oh snap! You're totally right! I'm not sure I've ever tried that. I did know that you can call a number of other array-methods on ColdFusion query columns: http://www.bennadel.com/blog/167 ... read »
InVision App - Prototyping Made Beautiful With Prototyping Tools