0

It is known that you should not customize the built-in prototypes in JavaScript. But, what is the best way to add custom properties/methods to specific instances of theses classes? Things to consider are performance, clean code, easy to use, drawbacks/limitations etc...

I have in mind the following 4 ways to to this, and you are free to present your own. In the examples I have only 1 array and only 2 methods to add, but the question is about many objects with many additinal properties/methods.

1st way: Add methods directly to objects

let myArray1 = [1,2,3,2,1];
myArray1.unique = () => [...new Set(myArray1)];   
myArray1.last = () => myArray1.at(-1);

2nd way, create a new (sub)class

class superArray extends Array{
    constructor() {
        super();
    } 
    unique(){return [...new Set(this)]}
    last(){return this.at(-1)}
}
let myArray2 = superArray.from([1,2,3,2,1]);

(The above example was updated, responding to @Bergi's comments)

3rd way, Object.assign

let superArrayMethods = {
    unique: function(){return [...new Set(this)]},
    last: function(){return this.at(-1)}
}
let myArray3 = [1,2,3,2,1];
myArray3 = Object.assign(myArray3, superArrayPrototype);

4th method, set prototype of an enhanced prototype of the Array prototype

let superArrayPrototype = {
    unique: function(){return [...new Set(this)]},
    last: function(){return this.at(-1)}
}
Object.setPrototypeOf(superArrayPrototype,Array.prototype);

let myArray4 = [1,2,3,2,1];
Object.setPrototypeOf(myArray4,superArrayPrototype);

In Chrome console, we see the four results:

enter image description here

The question!

What is the best way to add custom properties/methods to specific instances of built-in objects? And why?

Remember, the question is about creating many objects/arrays with (the same) many functions (that, of course, will be constructed/enhanced all together / in an organized fashion, not one by one as in these simplified examples!)

Does the same answer apply to other built-in objects, like HTML elements? What is the best way to add custom properties/methods to an HTML element? (maybe for another topic?)

Dim Vai
  • 726
  • 8
  • 14
  • This doesn't answer your question, But I'm partial to the class method, however, rather a than iterate every item you can simply push the array `super(); this.push(...sourceArray)`. Classes are essential just pretty wrappers for prototyping so it pretty much boils down to the same thing. – Pellay Apr 08 '22 at 11:03
  • @Pellay, I tried to do as you say, to simplify my code, but array methods do not work (or I do something wrong!). For example `myArray2.map(el=>el+1)` does not work ("sourceArray is not iterable"). The same with `.filter`. How do you get around this problem? Can you show me an example that works with `map`, `filter` etc? – Dim Vai Apr 08 '22 at 12:11
  • Sorry, I've updated my answer below. – Pellay Apr 08 '22 at 13:30
  • Thank you, I have added a comment on your reply. However, these comments here are about your comment that stated: "rather a than iterate every item you can simply push the array". Since there isn't any `push` in your answer, I consider your answer a different conversation than these comments here. Am I right? – Dim Vai Apr 08 '22 at 16:52
  • I've added a new answer, I learnt something today myself. Hopefully it'll help in your search. – Pellay Apr 08 '22 at 19:01
  • "*creating many objects/arrays with (the same) many functions, in an organized fashion*" - that seems to rule out approach 1. Unless you meant to package that in a function, but then you'd be creating a lot of closures, which is slower than just copying the method reference as in approach 3. – Bergi Apr 08 '22 at 19:22
  • 1
    Your second approach breaks the normal array methods. [You should not write an incompatible `constructor`](https://stackoverflow.com/a/64793718/1048572) - use `const myArray2 = SuperArray.from([1,2,3,2,1]);` instead. – Bergi Apr 08 '22 at 19:27
  • As for your fourth approach, have a look at [this question](https://stackoverflow.com/q/23807805/1048572) for why it might be slow. Also consider what would happen if multiple libraries would "extend" arrays in this way. – Bergi Apr 08 '22 at 19:31
  • @Bergi, I like your comments. You may post a separate answer to the question, suggesting your preferred way of extending built-in objects. – Dim Vai Apr 13 '22 at 19:43
  • @DimVai ... my two cent ... either go for sub-classing (recommended) or implement the methods correctly (non enumerable via `Reflect.defineProperty` or `Object.defineProperties`) at `Array.prototype`. I do not agree that enhancing `Array.prototype` is bad practice or would lead to braking code. – Peter Seliger Jul 24 '23 at 13:34

2 Answers2

0

Method 1 and Method 3 do the same thing. You're just defining functions against an instantiated array. If you're after speed simply stick with methods 1 or 3. Turns out classes extended from array and prototypes from arrays (which is the same thing under the hood) are simply very slow when it comes to large volumes of data. Small cases it's probably fine. I can't seem to find much info on this subject myself either. But hopefully this is a good starting point for further testing. It could take 10ish seconds to complete the test so be patient...

//Method 1
let myArray1 = [];
myArray1.unique = () => [...new Set(myArray1)];   
myArray1.last = () => myArray1.at(-1);
//Method 2
class superArray extends Array {
    constructor(...items) { super(...items) } 
    unique(){return [...new Set(this)]}
    last(){ return this.at(-1)}
}
let myArray2 = new superArray();
//Method 3
let superArrayMethods = {
    unique: function(){return [...new Set(this)]},
    last: function(){return this.at(-1)}
}
let myArray3 = [];
myArray3 = Object.assign(myArray3, superArrayMethods);
//Method 4
let superArrayPrototype = {
    unique: function(){return [...new Set(this)]},
    last: function(){return this.at(-1)}
}
Object.setPrototypeOf(superArrayPrototype,Array.prototype);
let myArray4 = [];
Object.setPrototypeOf(myArray4,superArrayPrototype);

//Timers
console.time("myArray1")
for(i=0; i<10000000; i++) { myArray1.push(i); }
console.timeEnd("myArray1")

console.time("myArray2")
for(i=0; i<10000000; i++) { myArray2.push(i); }
console.timeEnd("myArray2")

console.time("myArray3")
for(i=0; i<10000000; i++) { myArray3.push(i); }
console.timeEnd("myArray3")

console.time("myArray4")
for(i=0; i<10000000; i++) { myArray4.push(i); }
console.timeEnd("myArray4")


console.time("unique myArray1")
myArray1.unique();
console.timeEnd("unique myArray1")

console.time("unique myArray2")
myArray2.unique();
console.timeEnd("unique myArray2")

console.time("unique myArray3")
myArray3.unique();
console.timeEnd("unique myArray3")

console.time("unique myArray4")
myArray4.unique();
console.timeEnd("unique myArray4")

Many arrays, fewer items

As requested, here is a similar test, however we are now testing if having many array with fewer items has any bearing on the speed.

The following code seems to demonstrate a similar result; (Open Developer Tools (F12) to see an easier to read table of the results.

Like any test scripts, always run them a few times to validate;

//Method 1
let method1 = () => {
  let myArray1 = []
  myArray1.unique = () => [...new Set(myArray1)];   
  myArray1.last = () => myArray1.at(-1);
  return myArray1;
}

//Method 2
class superArray extends Array {
    constructor(...items) { super(...items) } 
    unique(){return [...new Set(this)]}
    last(){ return this.at(-1)}
}
let method2 = () => {
    return new superArray();
}

//Method 3
let superArrayMethods = {
    unique: function(){return [...new Set(this)]},
    last: function(){return this.at(-1)}
}
let method3 = () => {
    let myArray3 = [];
    myArray3 = Object.assign(myArray3, superArrayMethods);
  return myArray3;
}

//Method 4
let superArrayPrototype = {
    unique: function(){return [...new Set(this)]},
    last: function(){return this.at(-1)}
}
Object.setPrototypeOf(superArrayPrototype,Array.prototype);
let method4 = () => {
    let myArray4 = [];
    Object.setPrototypeOf(myArray4,superArrayPrototype);
  return myArray4;
}

let results = {};
let arrayBuilder = (method) => { 
    let preDefinedObjectsArray = Array.from({ length: itemsPerArray }, () => ({ test: "string" }));
    results[method.name] = { Creation: 0, Population: 0, CallingFunction:0 }
  //Test method array creation speed;
    let t0 = performance.now();
  for(let i = 0; i < numberOfArrays; i++) {
    let x = method();
  }
  let t1 = performance.now();
  results[method.name].Creation = (t1 - t0).toFixed(4);
  
  //Create an array of all arrays to test adding items and calling it's functions;
  let tmpArray = Array.from({ length: numberOfArrays }, () => (method()));
  
  //Test array population speed;
  t0 = performance.now();
  tmpArray.forEach(a => preDefinedObjectsArray.forEach(o => a.push(o)));
  t1 = performance.now();   
  results[method.name].Population = (t1 - t0).toFixed(4);
   
  //Test function calling on array;
  t0 = performance.now();
  tmpArray.forEach(a => a.unique());
  t1 = performance.now();   
  results[method.name].CallingFunction = (t1 - t0).toFixed(4);
  
  tmpArray = null;
  
}

const itemsPerArray = 100;
const numberOfArrays = 100000;
console.log(`Running test - Creating ${numberOfArrays} arrays with ${itemsPerArray} items per array per method. Be patient...`);
setTimeout(_ => { 
    [method1, method2, method3, method4].forEach(m => arrayBuilder(m));
    console.table(results);
  console.log(results);
  console.log('Open Console for clearer view of results (F12)')
},100);
.as-console-wrapper { max-height: 100% !important; }
Pellay
  • 792
  • 5
  • 13
  • I really like this answer and the way you approached my question. I will wait a little more in case other answers are posted, and then I will accept it. – Dim Vai Apr 13 '22 at 19:46
  • @DimVai Not a problem. I wasn't expecting to have an accepted answer from this. It generally interested me too, so I thought I'd test the methods too see if there was any major differences, as we discovered there are! :) – Pellay Apr 17 '22 at 17:02
  • I have acceped your answer. However I will ask you another question. Do we get the same results when we have a lot of small arrays (unlike your example where we have a few big ones)? I think this is a reasonable question as well – Dim Vai Apr 26 '22 at 17:14
  • I've added another set of tested to a new snippet for you. Enjoy! – Pellay Apr 27 '22 at 12:15
  • Thank you! This is an excellent and complete answer! – Dim Vai Apr 27 '22 at 19:01
0

Here's another idea: use Proxy to create extensions.

Small example:

const arrayX = function() {
 const notAllNrs = `not all numbers`
 return {
    unique: t => [...new Set(t)],
    last: t => t.at(-1),
    sum: t => !t.find(v => isNaN(+v)) 
      ? t.reduce( (acc, v) => acc + +v, 0) 
      : notAllNrs,
    max: t => !t.find(v => isNaN(+v)) 
      ? Math.max.call(...t) 
      : notAllNrs,
    min: t => !t.find(v => isNaN(+v)) 
      ? Math.min.call(...t)
      : notAllNrs,
  };
}();

const proxy = {
  get: (target, key) => {
    return arrayX[key]
    ? arrayX[key](target) 
    : target[key] instanceof Function 
      ? target[key]?.bind(target) 
      : target[key];
  }
}

const $A = obj => new Proxy(obj, proxy);

const arr = $A([1,2,1,1,22,1,19,7,3,4,5,7,22]);

console.log(`arr.unique: ${arr.unique}\narr.last: ${
  arr.last}\narr.sum ${arr.sum}\narr.max: ${arr.max}\narr.min: ${arr.min}`);

Here is a snippet to extend String, here a module/library extending Date.

KooiInc
  • 119,216
  • 31
  • 141
  • 177