ARTICLES

VIDEOS

BOOKS

FORUM

Extending Arrays

by kirupa   |   filed under Arrays

The versatility of arrays can't be overstated. On the surface, arrays allow us to store a collection of items. If we dig a bit deeper, arrays come with a buffet of capabilities to make dealing with a collection of items more productive...and maybe just a tad bit fun. As with all buffets, even those mega ones that feature foods from every part of the universe, there is always something missing. Something like that one capability where, if it were a part of what arrays (and buffets!) provide, everything would be perfect.

In this article, we will make a giant leap towards perfection. We will look at two ways we have to enhance the built-in capabilities of our humble Array by extending it with our own special capabilities. While not strictly required, brushing up on some topics like extending built-in objects and classes can be helpful in making sense of what we are going to be looking at.

Onwards!

Adding to the Prototype

Extending objects is something JavaScript has supported since the beginning of time. This OG approach involves relying on prototypes, which is the main way JavaScript objects inherit functionality from each other. Adding new methods or properties on the prototype ensures that all object instances of that prototype get those methods and properties as well. That last sentence will probably make more sense with an example, so let's revisit an example we have seen earlier.

Let's say we have a swap method that we would like to make available to all arrays. The way we can do that is by us adding the swap method to our array's prototype:

Array.prototype.swap = function(index_A, index_B) {
    let input = this;

    let temp = input[index_A];
    input[index_A] = input[index_B];
    input[index_B] = temp;
}

After defining this method, the way we call this swap method is no different than calling any other method on our array:

let myData = ["a", "b", "c", "d", "e", "f", "g"];

Array.prototype.swap = function(index_A, index_B) {
    let input = this;

    let temp = input[index_A];
    input[index_A] = input[index_B];
    input[index_B] = temp;
}

myData.swap(2, 5);
console.log(myData); // ["a", "b", "f", "d", "e", "c", "g"]

Despite our swap method being something we defined, using it as part our array feels totally natural - like it was always part of our array all this time! To better highlight this, the following is what the prototype chain for myData looks like:

Pay special attention to where the swap method is in relation to myData. From our myData array's point of view, the swap method is something that has always been there since it looks to its prototype and sees it there. It isn't just myData that has access to our swap method. Any arrays that we have defined in the same scope as our swap method have access to it as well:

If we define a new array calledcoolPeople, notice that it too is an instance of Array.prototype. That is to be expected. Because our Array.prototype.swap definition is in scope, swap is a part of the prototype that coolPeople sees as well. Neat, right?

Be Careful-ish in Extending Built-in Objects

I may sound like a broken record repeating this, but we have to be careful when extending built-in objects. The reason is that our extensions may break in the future when the JavaScript language defines a property whose name is the same as that of what we defined. For example, one could imagine that swap becomes something that all browsers support natively in the future. At that point, our version of swap would be redundant or, worse, have functionality that is different than what the browser provides. Le sigh! This doesn't mean that we should never extend built-in objects. It just means we have to consider the future impact of doing so. One easy solution is to pick a name for our extension (like kirupaSwap or myCoolSwapNoBrowserWouldCopy) that a browser will probably never use.

Subclassing our Array

Instead of adding more capabilities directly to our array prototype, we have a more modern approach where we can create our own Array-like object that extends our default Array. This is something known as subclassing where our custom array object has any custom methods/properties we define in addition to everything the base Array object provides. Take a look at the following example of this in action:

class AwesomeArray extends Array {
  swap(index_A, index_B) {
    let input = this;

    let temp = input[index_A];
    input[index_A] = input[index_B];
    input[index_B] = temp;
  }
}

We are defining a class called AwesomeArray that subclasses our Array by using the extends keyword. Inside AwesomeArray, we define our swap method. The way we can use this swap method is by creating an AwesomeArray object and then just calling swap on it:

class AwesomeArray extends Array {
  swap(index_A, index_B) {
    let input = this;

    let temp = input[index_A];
    input[index_A] = input[index_B];
    input[index_B] = temp;
  }
}

let myData = new AwesomeArray("a", "b", "c", "d", "e", "f", "g");
myData.swap(0, 1);
console.log(myData); // ["b", "a", "c", "d", "e", "f", "g"]

In the highlighted lines, we create our AwesomeArray object called myData. We create it by using the new keyword and calling the AwesomeArray constructor. Because AwesomeArray is still an Array behind the scenes, we can perform our usual array-like operations. For example, to initialize our array with some default values, we pass in our initial values as arguments to our AwesomeArray constructor:

let myData = new AwesomeArray("a", "b", "c", "d", "e", "f", "g");

This is just like what we can do with regular arrays. Now, what we can't do is use the bracket syntax which we have been using a bunch of times. We can't do this and expect our myData object to be an AwesomeArray:

let myData = ["a", "b", "c", "d", "e", "f", "g"];

The reason is that this syntax is designed to work only with the built-in Array type. If we were to use the bracket-based approach for creating our array, we would end up creating a traditional Array instead of an AwesomeArray with the swap method we defined. Using the explicit constructor-based approach for creating an object is our best solution for ensuring our array is awesome, an AwesomeArray. Overloading the bracket operator is sorta kinda possible using some cutting-edge JavaScript features like Proxies, but that's a rabbit hole I won't take you into today.

There is one more thing to cover when it comes to subclassing our array. A handful of array methods (such as map, filter, etc.) return an array as part of their regular operation. This array that gets returned respects the type of the array it was invoked from. This means calling map on our AwesomeArray type will return an array that is also an AwesomeArray:

class AwesomeArray extends Array {
    swap(index_A, index_B) {
      let input = this;
  
      let temp = input[index_A];
      input[index_A] = input[index_B];
      input[index_B] = temp;
    }
  }

let myData = new AwesomeArray("a", "b", "c", "d", "e", "f", "g");

let newData = myData.map((letter) => letter.toUpperCase());
console.log(newData); // ["A", "B", "C", "D", "E", "F", "G"]

console.log(newData.constructor.name) // AwesomeArray

We can verify this by checking the value of newData.constructor (where newData.constructor === AwesomeArraywill be true) or by just printing its name to our console like we did in our snippet above. This ability for our subclassed array to still maintain its subclassiness when dealing with methods that return arrays is very desirable. It means we can still party in our subclassed world while still taking advantage of powerful methods that exist in the base Array object at the same time.

Conclusion

We have two very fine approaches for extending our array with new and cooler capabilities. If you add to the array prototype, you can declare and use arrays like you always have. The benefit is that any properties or methods you added are available without you having to do anything extra. That may or may not be a good thing depending on who you ask. When you subclass and create your own array-based type, you are living life on the edge. The benefit of subclassing is that you are making an explicit choice in using your own custom specialized array. There are no accidental uses.

All in all, both approaches have their pros and cons. The pros certainly outweigh the cons in all cases, and if you squint really hard and dim the lights, both of these approaches are more similar than they are different. If you are still torn on which approach to use, flip a coin...and respect the outcome.

Got a question or just want to chat? Comment below or drop by our forums (they are actually the same thing!) where a bunch of the friendliest people you'll ever run into will be happy to help you out!

When Kirupa isn’t busy writing about himself in 3rd person, he is practicing social distancing…even on his Twitter, Facebook, and LinkedIn profiles.

Hit Subscribe to get cool tips, tricks, selfies, and more personally hand-delivered to your inbox.

COMMENTS

Serving you freshly baked content since 1998!
Killer hosting by (mt) mediatemple

Twitter Youtube Facebook Pinterest Instagram Github