user1272965
user1272965

Reputation: 3064

Subclassing in Javascript

I'm trying to make a subclass of an image library on github called Jimp. As far as I can tell from the docs, you don't instantiate the class in the usual way. Instead of saying new Jimp(), it seems the class has a static method called read that acts as a constructor. From the docs...

Jimp.read("./path/to/image.jpg").then(function (image) {
    // do stuff with the image
}).catch(function (err) {
    // handle an exception
});

It looks like from the docs, that that image returned by read() is an instance allowing the caller to do stuff like image.resize( w, h[, mode] ); and so on.

I'd like to allow my subclass callers to begin with a different static method that reads an image and does a bunch of stuff, summarized as follows...

class MyJimpSubclass extends Jimp {

    static makeAnImageAndDoSomeStuff(params) {
        let image = null;
        // read in a blank image and change it
        return Jimp.read("./lib/base.png").then(_image => {
            console.log(`image is ${_image}`);
            image = _image;
            let foo = image.bar();  // PROBLEM!
            // ...
            // ...
        .then(() => image);
    }

    bar() {
        // an instance method I wish to add to the subclass
    }

// caller
MyJimpSubclass.makeAnImageAndDoSomeStuff(params).then(image => {
    //...
});

You might be able to guess that nodejs gets angry on the line let foo = image.bar();, saying

TypeError image.bar is not a function

.

I think this is understandable, because I got that image using Jimp.read(). Of course that won't return an instance of my subclass.

  1. First idea: Change it to MyJimpSubclass.read(). Same problem.
  2. Second idea: Implement my own static read method. Same problem.

    static read(params) {
        return super.read(params);
    }
    
  3. Third idea: Ask SO

Upvotes: 1

Views: 158

Answers (3)

Barmar
Barmar

Reputation: 781028

This might be a way to do it. Start with your own read method, and have it change the prototype of the returned object.

static read(...params) {
    return super.read(...params).then(image) {
        image.prototype = MyJimpSubclass;
        resolve(image);
    }
}

Upvotes: 1

Ry-
Ry-

Reputation: 224921

The implementation of Jimp.read refers to Jimp specifically, so you would have to copy and change it in your subclass (ick, but not going to break anything since the constructor is also part of the API) or make a pull request to have it changed to this and have subclassing explicitly supported:

static read(src) {
    return new Promise((resolve, reject) => {
        void new this(src, (err, image) => {
            if (err) reject(err);
            else resolve(image);
        });
    });
}

Alternatively, you could just implement all your functionality as a set of functions on a module. This would be next on my list after making a pull request. Would not recommend a proxy.

const makeAnImageAndDoSomeStuff = (params) =>
    Jimp.read("./lib/base.png").then(image => {
        console.log(`image is ${image}`);
        let foo = bar(image);
        // …
        return image;
    });

function bar(image) {
    // …
}

module.exports = {
    makeAnImageAndDoSomeStuff,
    bar,
};

Even changing the prototype would be better than a proxy (but this is just a worse version of the first option, reimplementing read):

static read(src) {
    return super.read(src)
        .then(image => {
            Object.setPrototypeOf(image, this.prototype);
            return image;
        });
}

Upvotes: 1

samanime
samanime

Reputation: 26547

You have a couple of options. The cleanest is probably to make a subclass like you started, but then implement the Jimp static method on it, as well as your own. In this case, it's not really inheritance, so don't use extends.

class MyJimp {
  static read(...args) {
    return Jimp.read.apply(Jimp, args);
  }

  static makeAnImage(params) {
    return this.read(params)
      .then(image => {
        // do stuff
        return image
      });
  }
}

From there, I would make an object which has all of the new functions you want to apply to image:

const JimpImageExtension = {
  bar: () => { /* do something */ }
};

Finally, in your static methods, get the image and use Object.assign() to apply your new functions to it:

class MyJimp {
  static read(...args) {
    return Jimp.read.apply(Jimp, args)
      .then(image => Object.assign(image, JimpImageExtension));
  }

  static makeAnImage(params) {
    return this.read(params)
      .then(image => {
        // do stuff
        image.bar();
        return image;
      });
  }
}

This should do the trick by applying your extra functions to the image. You just need to make sure that you apply it at every point that can generate an image (if there is more than just read). Since in the other functions, it's using your version of read(), you only need to add the functions in the one.

Another approach would be if Jimp makes their image class accessible, you could also add them to the prototype of that (though usually in libraries like this, that class is frequently inaccessible or not actually a class at all).

Upvotes: 1

Related Questions