Exploring the World of Subclassing Arrays in JavaScript: Uncovering the TypeError of Array.prototype.toString's

Can JavaScript Arrays be subclassed and inherited from?

I am interested in creating my own custom Array object that not only has all the features of a regular Array but also contains additional properties. My intention is to use myobj instanceof CustomArray for specific operations if the instance is my CustomArray.

After encountering some difficulties while attempting to subclass, I came across an article by Dean Edwards that suggests working with Array objects in this manner does not function correctly, especially in Internet Explorer. However, I have encountered other issues as well (although I have only tested it in Chrome so far).

Here is an example of the code I tried:

/** 
 *  Inherit the prototype methods from one constructor into another 
 *  Borrowed from Google Closure Library 
 */
function inherits(childCtor, parentCtor) {
    function tempCtor() {};
    tempCtor.prototype = parentCtor.prototype;
    childCtor.superClass_ = parentCtor.prototype;
    childCtor.prototype = new tempCtor();
    childCtor.prototype.constructor = childCtor;
},

// Custom class that extends Array class
function CustomArray() {
    Array.apply(this, arguments);
}
inherits(CustomArray,Array);

array = new Array(1,2,3);
custom = new CustomArray(1,2,3);

When executed in Chrome's console, the following output is produced:

> custom
[]
> array
[1, 2, 3]
> custom.toString()
TypeError: Array.prototype.toString is not generic
> array.toString()
"1,2,3"
> custom.slice(1)
[]
> array.slice(1)
[2, 3]
> custom.push(1)
1
> custom.toString()
TypeError: Array.prototype.toString is not generic
> custom
[1]

It is clear that the behavior of the objects is different. Should I abandon this approach altogether, or is there a way to achieve my goal of myobj instanceof CustomArray?

Answer №1

In a recent publication by Juriy Zaytsev, better known as @kangax, he delves into the topic with fresh insights.

Through his exploration, Zaytsev covers a range of alternatives such as Dean Edwards' iframe borrowing technique, direct object extension, prototype extension, and leveraging ECMAScript 5 accessor properties.

The conclusion? There's no one-size-fits-all solution; each method comes with its own set of advantages and limitations.

This article is definitely worth checking out:

  • Discover why ECMAScript 5 still lacks subclassing for arrays

Answer №2

ES6

class SubArray extends Array {
    last() {
        return this[this.length - 1];
    }
}
var sub = new SubArray(1, 2, 3);
sub // [1, 2, 3]
sub instanceof SubArray; // true
sub instanceof Array; // true

New Approach: (Highly efficient and recommended)

Referencing the concepts from a article discussed in the accepted answer for further understanding

Using ES6 Inheritance

function SubArray() {
  let arr = [ ];
  arr.push.apply(arr, arguments);
  Object.setPrototypeOf(arr, SubArray.prototype);
  return arr;
}
SubArray.prototype = Object.create(Array.prototype);

You can now define custom methods for SubArray

SubArray.prototype.last = function() {
  return this[this.length - 1];
};

Initialization similar to standard Arrays

let sub = new SubArray(1, 2, 3);

Operates like regular Arrays

sub instanceof SubArray; // true
sub instanceof Array; // true

Answer №3

Attempting this type of task in the past has proven to be challenging, as it rarely yields successful results. However, one can potentially simulate the desired outcome by internally utilizing Array.prototype methods. This unique CustomArray class, which has only been tested in Chrome thus far, incorporates both the standard push method and a custom method called last. (Interestingly, I never thought of using this approach before xD)

function CustomArray() {
    this.push = function () {
        Array.prototype.push.apply(this, arguments);
    }
    this.last = function () {
        return this[this.length - 1];
    }
    this.push.apply(this, arguments); // implement "new CustomArray(1,2,3)"
}
a = new CustomArray(1,2,3);
alert(a.last()); // 3
a.push(4);
alert(a.last()); // 4

If you wish to incorporate any other Array methods into your customized implementation, they will need to be manually added. Using loops could be a clever workaround, considering that the logic within our custom push function is quite generic.

Answer №4

Check out this code snippet. It functions properly in all browsers that support the use of '__proto__'.

var getPrototypeOf = Object.getPrototypeOf || function(o){
    return o.__proto__;
};
var setPrototypeOf = Object.setPrototypeOf || function(o, p){
    o.__proto__ = p;
    return o;
};

var CustomArray = function CustomArray() {
    var array;
    var isNew = this instanceof CustomArray;
    var proto = isNew ? getPrototypeOf(this) : CustomArray.prototype;
    switch ( arguments.length ) {
        case 0: array = []; break;
        case 1: array = isNew ? new Array(arguments[0]) : Array(arguments[0]); break;
        case 2: array = [arguments[0], arguments[1]]; break;
        case 3: array = [arguments[0], arguments[1], arguments[2]]; break;
        default: array = new (Array.bind.apply(Array, [null].concat([].slice.call(arguments))));
    }
    return setPrototypeOf(array, proto);
};

CustomArray.prototype = Object.create(Array.prototype, { constructor: { value: CustomArray } });
CustomArray.prototype.append = function(var_args) {
    var_args = this.concat.apply([], arguments);        
    this.push.apply(this, var_args);

    return this;
};
CustomArray.prototype.prepend = function(var_args) {
    var_args = this.concat.apply([], arguments);
    this.unshift.apply(this, var_args);

    return this;
};
["concat", "reverse", "slice", "splice", "sort", "filter", "map"].forEach(function(name) {
    var _Array_func = this[name];
    CustomArray.prototype[name] = function() {
        var result = _Array_func.apply(this, arguments);
        return setPrototypeOf(result, getPrototypeOf(this));
    }
}, Array.prototype);

var array = new CustomArray(1, 2, 3);
console.log(array.length, array[2]);//3, 3
array.length = 2;
console.log(array.length, array[2]);//2, undefined
array[9] = 'qwe';
console.log(array.length, array[9]);//10, 'qwe'
console.log(array+"", array instanceof Array, array instanceof CustomArray);//'1,2,,,,,,,,qwe', true, true

array.append(4);
console.log(array.join(""), array.length);//'12qwe4', 11

Answer №5

Here is a comprehensive example that is compatible with ie9 and newer versions. For older versions like <=ie8, alternative implementations of Array.from, Array.isArray, etc., would be required. The following example demonstrates:

  • Placing the Array subclass in its own closure (or Namespace) to prevent conflicts and namespace pollution.
  • Inheriting all prototypes and properties from the native Array class.
  • Showcasing how to define additional properties and prototype methods.

If you have access to ES6, it is recommended to use the class SubArray extends Array method as suggested by laggingreflex.

Below are the essentials for subclassing and inheriting from Arrays. Following this snippet is the complete example.

///Collections functions as a namespace.     
///_NativeArray to avoid naming conflicts. All references to Array within this closure refer to the Array function declared inside.     
var Collections = (function (_NativeArray) {
    //__proto__ is deprecated but Object.xxxPrototypeOf isn't as widely supported. '
    var setProtoOf = (Object.setPrototypeOf || function (ob, proto) { ob.__proto__ = proto; return ob; });
    var getProtoOf = (Object.getPrototypeOf || function (ob) { return ob.__proto__; });

    function Array() {          
        var arr = new (Function.prototype.bind.apply(_NativeArray, [null].concat([].slice.call(arguments))))();
        setProtoOf(arr, getProtoOf(this));     
        return arr;
    }

    Array.prototype = Object.create(_NativeArray.prototype, { constructor: { value: Array } });
    Array.from = _NativeArray.from; 
    Array.of = _NativeArray.of; 
    Array.isArray = _NativeArray.isArray;

    return { //Methods exposed externally
        Array: Array
    };
})(Array);

Complete example:

///Collections functions as a namespace.     
///_NativeArray to prevent naming conflicts. All references to Array in this closure point to the Array function declared inside.     
var Collections = (function (_NativeArray) {
    //__proto__ is deprecated but Object.xxxPrototypeOf isn't as widely supported.
    var setProtoOf = (Object.setPrototypeOf || function (ob, proto) { ob.__proto__ = proto; return ob; });
    var getProtoOf = (Object.getPrototypeOf || function (ob) { return ob.__proto__; });        

    function Array() {          
        var arr = new (Function.prototype.bind.apply(_NativeArray, [null].concat([].slice.call(arguments))))();           
        setProtoOf(arr, getProtoOf(this));
        return arr;
    }

    Array.prototype = Object.create(_NativeArray.prototype, { constructor: { value: Array } });
    Array.from = _NativeArray.from; 
    Array.of = _NativeArray.of; 
    Array.isArray = _NativeArray.isArray;

    //Add some convenient properties.
    Object.defineProperty(Array.prototype, "count", { get: function () { return this.length - 1; } });
    Object.defineProperty(Array.prototype, "last", { get: function () { return this[this.count]; }, set: function (value) { return this[this.count] = value; } });

    //Add some convenient Methods.    
    Array.prototype.insert = function (idx) {
        this.splice.apply(this, [idx, 0].concat(Array.prototype.slice.call(arguments, 1)));
        return this;
    };
    Array.prototype.insertArr = function (idx) {
        idx = Math.min(idx, this.length);
        arguments.length > 1 && this.splice.apply(this, [idx, 0].concat([].pop.call(arguments))) && this.insert.apply(this, arguments);
        return this;
    };
    Array.prototype.removeAt = function (idx) {
        var args = Array.from(arguments);
        for (var i = 0; i < args.length; i++) { this.splice(+args[i], 1); }
        return this;
    };
    Array.prototype.remove = function (items) {
        var args = Array.from(arguments);
        for (var i = 0; i < args.length; i++) {
            var idx = this.indexOf(args[i]);
            while (idx !== -1) {
                this.splice(idx, 1);
                idx = this.indexOf(args[i]);
            }
        }
        return this;
    };

    return { //Methods exposed externally
        Array: Array
    };
})(Array);

Below are some usage examples and tests.

var colarr = new Collections.Array("foo", "bar", "baz", "lorem", "ipsum", "lol", "cat");
var colfrom = Collections.Array.from(colarr.reverse().concat(["yo", "bro", "dog", "rofl", "heyyyy", "pepe"]));
var colmoded = Collections.Array.from(colfrom).insertArr(0, ["tryin", "it", "out"]).insert(0, "Just").insert(4, "seems", 2, "work.").remove('cat','baz','ipsum','lorem','bar','foo');

colmoded; //["Just", "tryin", "it", "out", "seems", 2, "work.", "lol", "yo", "bro", "dog", "rofl", "heyyyy", "pepe"]

colmoded instanceof Array; //true

Answer №6

Implementing a Custom Constructor with ES6

If you're looking to customize the constructor in an ES6 class, there are some considerations to keep in mind, especially when it comes to maintaining old methods.

By following the guidance provided at: How can I extend the Array class and retain its implementations we can achieve the following:

#!/usr/bin/env node

const assert = require('assert');

class MyArray extends Array {
  constructor(elements, number) {
    super(...elements);
    this.number = number;
  }

  static get [Symbol.species]() {
    return Object.assign(function (...items) {
      return new MyArray(new Array(...items))
    }, MyArray);
  }

  increment() { return this.number + 1; }
}

const customArray = new MyArray([2, 3, 5], 9);
assert(customArray[0] === 2);
assert(customArray[1] === 3);
assert(customArray[2] === 5);

assert(customArray.number === 9);

assert(customArray.increment() === 10);

assert(customArray.toString() === '2,3,5');

slicedArray = customArray.slice(1, 2);
assert(slicedArray[0] === 3);
assert(slicedArray.constructor === MyArray);

For a method to simulate index notation [] without using Array, refer to: Implementing Array-like behavior in JavaScript without relying on Array

This code was tested in Node.js v10.15.1.

Answer №7

Check out my new NPM module called inherit-array, designed to simplify this process. Here's a brief overview of what it does:

To create a subclass of an array:
function toArraySubClassFactory(ArraySubClass) {
  ArraySubClass.prototype = Object.assign(Object.create(Array.prototype),
                                          ArraySubClass.prototype);

  return function () {
    var arr = [ ];
    arr.__proto__ = ArraySubClass.prototype; 

    ArraySubClass.apply(arr, arguments);

    return arr;
  };
};

Once you have your own SubArray class ready, inherit from the Array using the following code snippet:

var SubArrayFactory = toArraySubClassFactory(SubArray);

var mySubArrayInstance = SubArrayFactory(/*whatever SubArray constructor takes*/)

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

How can I receive user input in JavaScript while incorporating three.js?

I am currently exploring the functionalities of this three.js example () and I am interested in enabling users to input specified X, Y, and Z positions for boxes dynamically. Initially, I considered utilizing a JavaScript prompt like below: var boxes = p ...

What is the best way to redirect to a specific page when logging out of a website?

To begin, let me outline the situation at hand. We have two distinct websites - website-A and website-B. Each of these websites is hosted on separate domains. A user has the ability to log in to website-B's homepage through the login page of webs ...

How can I specify the exact width for Bootstrap 3 Progress Bars?

I have a single page that displays all my files in a table format, and I also have the count of open and closed files. My goal is to represent the percentage of closed files using a progress bar. The width of the progress bar should change based on this p ...

Locate all the properties that are empty within each object contained in a JavaScript array

Consider this scenario: if I were to have an array of JavaScript objects like the following: var jsObjects = [ {a: 1, b: 2, c: null, d: 3, e: null}, {a: 3, b: null, c: null, d: 5, e: null}, {a: null, b: 6, c: null, d: 3, e: null}, {a: null, ...

Show a collection of elements containing attributes in JSX

I have a React component where I am displaying data from an array called pkgData: <div className="mainApp"> <div className="pkgsList"> {pkgData.map((p) => <h3 key={p.name}>{p.name}</h3> ...

AngularJS - Multi-controller Data Calculation

I am currently in the process of developing an Angularjs application. The project is quite extensive and requires segmentation into multiple Controllers to manage effectively. One challenge I am facing is performing calculations across these controllers. ...

Retrieve only the initial tag content using jquery

My goal is to extract the "22" from the following code... <div class="left"> <a class="count-link" href="http://url1.com"> <span>22</span> users </a> <a class="count-link" href="http://url2.com"> <span>10</span ...

An issue arises with the Datatables destroy function

Utilizing datatables.js to generate a report table on my page with filters. However, when applying any of the filters, the data returned has varying column counts which prompts me to destroy and recreate the table. Unfortunately, an error message pops up ...

Tips for altering Koa's HTTP status code for undeclared paths

If an undefined route is accessed on a Koa server, what is the best method to change the default HTTP status code and response body? Currently, Koa returns a 404 status and 'Not Found' text in the body. I intend to modify this to 501 (Not implem ...

I must create text that is transparent against a colorful gradient background

Hey there! I'm seeking help in figuring out how the text on this site is designed. You can take a look at it here. Essentially, what I'm aiming for is to have the text color match the gradient of the background color from the previous div, while ...

Load the iframe element only when the specified class becomes visible within the container div, without relying on jQuery

I am facing a unique challenge where the performance of my page is significantly impacted by loading multiple iframes. These iframes are contained within popup modals, and I would like to delay their loading until a user clicks on the modal. When the mod ...

Error: 'delay' is not recognized as a valid function

I'm encountering an error message that reads as follows: $("div.valid_box").html(data.message).css("margin-left", "145px").css("width", "520px").show().delay is not a function [Break On This Error] $(...elay(10000).hide("slow", function() { within m ...

Sending an AJAX request to a REST service in order to submit the information captured in an HTML form

<html> <body> <form method="POST"> <label>username</lable> <input id="username" name="username" type="text"> <label>emailid</lable> <input id="emailid" ...

What is the most effective method to arrange absolute divs generated randomly in a grid-like formation?

Hey there! I'm facing an intriguing challenge with my app. It's designed to generate a variable number of divs which are always in absolute positioning. Unfortunately, making them relative is not an option due to some other factors within the app ...

Ensuring uniform sizing of anchor text using jQuery

My goal is to creatively adjust the font size of anchor text within a paragraph, making it appear as though the text is moving towards and away from the viewer without affecting the surrounding paragraph text. Currently, I am encountering an issue where th ...

I'm interested in developing a React function that generates recipe components based on a set of instructions provided in an array, along with a separate parameter specifying the recipe name

I am currently immersed in the book "Learning React" written by O'Reilly. The book mentions a method of creating components by using a function known as the "component creating function". It advises supplying the necessary parameters as the second par ...

Incorporating asynchronous file uploads to a server using a for loop

I am in the process of transforming my original code into "async" code. The initial code queries the database and retrieves the results, which includes images. I want to optimize writing the images asynchronously to my nodejs server as the current synchro ...

What is the process for retrieving information from a retail outlet?

How can I retrieve data from the Vuex store using Vue.js? Below is my code: Vue.use(Vuex); export default new Vuex.Store({ modules: { data } }) data.js import axios from 'axios'; const state = { data: '' }; cons ...

TypeScript's type inference feature functions well in scenario one but encounters an error in a different situation

I recently tried out TypeScript's type inference feature, where we don't specify variable types like number, string, or boolean and let TypeScript figure it out during initialization or assignment. However, I encountered some confusion in its be ...

Modify the CSS when CKEditor is in focus

Implementing CKEditor in a symfony project using the FOS\CKEditor-bundle 1.2. I want to style the entire element containing CKEditor with a border when it is focused, similar to how questions are written or answered on Stackoverflow. By referencing a ...