3

I thought it would be useful to extend Arrays in javascript, to enhance them with some custom methods and business logic. It worked fine so far until I tried to filter on my instances.

It seems like the array filter method calls my constructor with one argument (0 if nothing is found) and returns a new instance. Not what I expected.

class MyArray extends Array {
  constructor(x, y, z) {
    super(x, y, z);
    // do custom stuff
  }
}

let numbers = new MyArray(1, 2, 3);
console.log(numbers.filter((v) => v === 0));

Though, if I pass and spread some arguments into a single variable, everything works.

Technically I see no difference between the two, I don't get it!

class MyArray extends Array {
  constructor(x, ...args) {
    super(x, ...args);;
    // do custom stuff
  }
}

let numbers = new MyArray(1, 2, 3);
console.log(numbers.filter((v) => v === 0));
  1. Why does the first approach run into this issue?
  2. Why does the second approach not run into this?

(Is it bad practice to extend Arrays or other core js classes?)

Basti
  • 606
  • 1
  • 12
  • 22
  • 1
    Does this answer your question? [Ways to extend Array object in javascript](https://stackoverflow.com/questions/11337849/ways-to-extend-array-object-in-javascript) – Andy Ray May 24 '21 at 17:01
  • 5
    The Array constructor can be called with one argument specifying the desired length, or more arguments to specify the initial array elements. `filter()` is using the first method to create an empty array that it will then push onto. – Barmar May 24 '21 at 17:02
  • 3
    I would highly suggest against extending built in objects in Javascript. Any other libraries in your codebase could break because you are hijacking default objects. I would also suggest working with pure functions, which Javascript is better suited to, instead of trying to extend classes. Don't try to make arrays smarter, try to write your business logic in functions that use arrays as the implementation detail. – Andy Ray May 24 '21 at 17:02
  • 1
    @AndyRay OP is subclassing Array, that's fine, they're not monkeypatching the prototype. – Teemu May 24 '21 at 17:04
  • Alternatively if you want `filter` not to construct `MyArray` you can set [`@@species`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/@@species) to `Array`. – Patrick Roberts May 24 '21 at 17:04
  • @Teemu ah good point, my comment about breaking other libraries is invalid. I still advise against this pattern, classes only make things more confusing here, functions that manipulate arrays (or whatever types of data they choose to encapsulate) are simpler. – Andy Ray May 24 '21 at 17:05
  • I'm skeptical this is a good use of inheritance-over-composition without any context. – Dave Newton May 24 '21 at 17:09
  • Actually, in my current context, I benefit from extending an array because I am able to track changes on arrays for my reactive ui, where class instances are not supported. – Basti May 24 '21 at 17:12
  • @PatrickRoberts also thanks for your comment, this did also help to solve my problems! – Basti May 24 '21 at 17:46

1 Answers1

4

The Array constructor behaves differently depending on the number and type of arguments it receives.

If it receives a single argument containing an integer, this argument is the initial size of the array. It returns an array that size, with the elements uninitialized.

In other cases, the arguments are the initial elements of the array.

So when you call super(x, y, z), you're using the second format, and it creates an array initially containing the values x, y, z.

filter() calls your constructor using the first format, so x is the size of the array it requested (0 as you said). But your constructor has additional arguments y and z, so those are being set to undefined. So you're calling super(0, undefined, undefined), which is creating an array [0, undefined, undefined].

Your second constructor is almost right, but the real Array constructor can be called with no arguments, returning an empty array. If your constructor is called that way, it will call super(undefined), creating an array with a single element containing undefined. The right way to define the constructor is with a single spread argument. This won't make a difference for filter(), but might affect other code.

class MyArray extends Array {
  constructor(...args) {
    super(...args);;
    // do custom stuff
  }
}

let numbers = new MyArray(1, 2, 3);
console.log(numbers.filter((v) => v === 0));
Barmar
  • 741,623
  • 53
  • 500
  • 612
  • Thanks for taking the time for this explanation! It helps a lot and I will be able to adress my issues with this solution! – Basti May 24 '21 at 17:18