Search code examples
javascriptclassinheritancenew-operator

How to create a new object of same class as current object from within a method


I have a sub-class of Array that I'm using for an internal project. Some of the methods I add need to return a new array. I'm trying to figure out what's the best way to create that new array.

I don't want to hard-code it to create an array of my specific subclass because if someone else subclasses my class and that's the source array class, then it should create an object of that subclass. In other words, I want to create a new object that is the same class as the current this object no matter how many other subclasses of that there are below mine.

The Array class itself already does something like this. If you sub-class Array and then use the normal .map() function on an instance of your sub-class, it returns a new Array using your class.

The ECMAScript spec for this is here for .map() and here for ArraySpeciesCreate() that .map() uses, but I can't figure out what all this spec logic translates to in terms of actual real-world Javascript code.

Right now, I'm just using:

let newArray = new this.constructor();

and it seems to work in my own little world, but I'm wondering if all that logic in ArraySpeciesCreate() should involve more code than this?

FYI, here's ArraySpeciesCreate() from the ECMAScript spec which .map() is supposed to follow to create the new array it returns. That is what I'm presumably trying to follow also.

enter image description here

What actual Javascript code would one use to implement this in your own class?


Here's an example of a method from my Array subclass:

// break an array up into chunks
// the last chunk may have fewer items
// returns an array of arrays
chunk(chunkSize) {
    if (!Number.isInteger(chunkSize) || chunkSize <= 0) {
        throw new TypeError('chunkSize must be a positive integer');
    }
    const output = new this.constructor();
    const numChunks = Math.ceil(this.length / chunkSize);
    let index = 0;
    for (let i = 0; i < numChunks; i++) {
        output.push(this.slice(index, index + chunkSize));
        index += chunkSize;
    }
    return output;
}

This line of code in that method:

const output = new this.constructor();

is the one I'm asking about that is supposed to implement the ArraySpeciesCreate() logic.


Solution

  • I still hold the opinion that you shouldn't subclass Array, but I can show how ArraySpeciesCreate would look if implemented in ECMAScript:

    if (!Array.isArray(this))               // if not called on an array
        return new Array(len);
    
    const {constructor} = this;
    if (typeof constructor == "function"    // if the constructor looks like a constructor,
      && !Function.prototype.isPrototypeOf(constructor) // but appears to stem from a
                                            // different realm (iframe etc)
      && constructor.name == "Array")       // and might be that realm's builtin Array
        return new Array(len);
    
    const C = constructor?.[Symbol.species];
    if (C == null)                          // if there's no subclass species
        return new Array(len);
    
    return new C(len);
    

    You probably can leave out the weird edge case of testing for cross-realm instances, which isn't really working precisely anyway. (I doubt there's a good way to check for these, and it seems impossible to reproduce the GetFunctionRealm steps - though maybe you want to throw in some check for constructor being a native function).

    In general, it just comes down to accessing Symbol.species on this.constructor, and using the result of that instead of the current class for constructing the new instance.

    Alternatively, you could just cheat and use Array.prototype.slice.call(this, 0, 0) :-)

    Another good solution is the ArraySpeciesCreate function from the es-abstract library, which tries to implement abstract operations as precise as possible.