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

Tips on how to modify database records rather than generating new ones

Currently, my team is developing a web application that utilizes wearables to monitor vital parameters. As part of our integration testing, we are using a Fitbit device. The app itself is built with Angular and JavaScript, while the database is hosted on C ...

What is the best approach to identify duplicated objects within an array?

I have an array with a specific structure and I am looking to add non-duplicate objects to it. [ { applicationNumber: "2", id: "8cca5572-7dba-49de-971b-c81f77f221de", country: 23, totalPrice: 36 }, { applicationNumber: "3", id: "8cc ...

Issues arise with the loading of models while attempting to utilize Mocha testing

I'm currently in the process of using mocha for testing my express application. In terms of folder structure, it looks like this: myapp |-app |--models |-test |--mocha-blanket.js |--mocha |--karma |-server.js The server.js file serves as my express ...

Adjust the background hue and opacity when incorporating a "modal div"

Currently, I have a page where a hidden div appears at some point to display a form for inputting data. This could be considered a modal div. My goal is to darken the rest of the page and only allow interaction with this specific div. I want the backgroun ...

Error encountered in VUE JS 3: The function vue.initDirectivesForSSR is not recognized as a valid function

I've been encountering some errors while trying to build my Vue web app with this configuration. I have searched for a solution, but couldn't find anyone facing the same issue as me. Any suggestions on how to resolve this problem? The build was s ...

Exploring the Functionality of worker-loader Inline

It was my understanding that the Webpack worker-loader configuration below: ... module: { rules: [ { test: /worker\.js/, loader: "worker-loader", options: { inline: 'fallba ...

Eliminate unneeded null and empty object data attributes within Vue.js

When sending object data to an API, I have multiple states but not all of them are required every time. Therefore, I would like to remove any properties from the object that are null or empty strings. How can I achieve this? export default { methods: { ...

The technique for handling intricate calls in node.js

My goal is to create a social community where users are rewarded for receiving upvotes or shares on their answers. Additionally, I want to send notifications to users whenever their answers receive some interaction. The process flow is detailed in the com ...

A guide on removing an element from a state array in a React functional component for a To-Do List application

I need help making a to-do list item disappear when clicked. The deleteHandler method is not working as expected. Can anyone provide logic on how to filter out the clicked item? import React, { useState } from 'react'; const ToDoList = () => ...

Does ECMAScript differentiate between uppercase and lowercase letters?

It has come to my attention that JavaScript (the programming language that adheres to the specification) is sensitive to case. For instance, variable names: let myVar = 1 let MyVar = 2 // distinct :) I have not found any evidence in the official specific ...

Unexpected error from API call detected within Mint localhost Template Kits elements

Hello Everyone, hope you're all having a good day! Recently, I installed the Elementor and Envato Elements plugins on my localhost Linux Mint WordPress setup. After configuring permalinks and other settings in WordPress, I proceeded to browse Templat ...

Learn how to implement Redux login to display the first letter of a user's name and their login name simultaneously

I am implementing a Redux login feature and after successful login, I want to display the first letter of the user's name or email in a span element within the header section. Can anyone provide a solution for this? Below is my Redux login code snippe ...

Using TypeScript, what is the best way to call a public method within a wrapped Component?

Currently, I'm engaged in a React project that utilizes TypeScript. Within the project, there is an integration of the react-select component into another customized component. The custom wrapped component code is as follows: import * as React from " ...

What is the best way to shorten text in React/CSS or Material UI based on the line height multiples?

I'm facing an issue with creating a Material UI card that contains text. My goal is to set a fixed height for the card and truncate the text if it exceeds 3 lines. Can anyone suggest the best approach to achieve this? Here's the code snippet I&a ...

What is the process for crafting a Vuejs controlled input form?

Although I am familiar with creating controlled components in React using state, I am new to the Vue framework and would like to understand how to adapt my code from React to be compatible with Vue. My goal is to store the values of input fields in a data ...

Switching between different sections of a webpage

Seeking assistance with implementing a transition effect between sections on a single-page application. All sections are located on the same page, but only one section is displayed at a time. When an event occurs, the display property of the requested sect ...

How can I optimize Javascript and CSS that are being used on my website but are not physically hosted on my website?

On my website, I have a plugin called "Contact Us" that was created and is being operated by Dropifi. Lately, I've been working on optimizing my site for SEO/Speed using Google's PageSpeed Insights tool. I enabled compression with GZip for all el ...

Initiating a click function for hyperlink navigation

Here is the HTML and JavaScript code that I am currently working with: <!DOCTYPE html> <html> <head> <script src="http://code.jquery.com/jquery-3.3.1.min.js"></script> </head> <body> <a href="#f ...

The Highcharts plugin is currently not able to render the data

I have been extracting data from the mtgox api and after analyzing my console, I can confirm that all the data is being properly delivered to my chart. Nevertheless, I am facing difficulties in getting the data to actually display on my chart. Any assist ...

Utilizing ngModel within the controller of a custom directive in Angular, instead of the link function

Is there a way to require and use ngModel inside the controller of a custom Angular directive without using the link function? I have seen examples that use the link function, but I want to know if it's possible to access ngModel inside the directive ...