4
\$\begingroup\$

I'm investigating using prototypes in JavaScript to simulate using 'base classes' which can be 'extended'. I've created an example where dialogBox is my 'base' and welcomeMessage is the 'extension'.

// OBJECT CONSTRUCTORS

var dialogBox = function(xPosition, yPosition, width, height){
    this.x = xPosition;
    this.y = yPosition;
    this.width = width;
    this.height = height;
    this.draw = function(){
        STAGE.fillRect(this.x, this.y, this.width, this.height);
        STAGE.strokeRect(this.x, this.y, this.width, this.height);        
    };
};

var welcomeMessage = function(messageString){
    this.message = messageString;
    this.addMessage = function(){
        STAGE.strokeText(this.message, this.x + 10, this.y + 10, this.width-10);
        console.log(this.message);
    };
};
welcomeMessage.prototype = new dialogBox();

This works as intended (or at least gives the appearance of working as intended) - see this fiddle.

However, the way I have to instantiate these objects seems rather inelegant:

var box1 = new dialogBox(20, 20, 300, 100);

var box2 = new welcomeMessage('hello there');
box2.x = 20;
box2.y = 140;
box2.width = 300;
box2.height = 100; // wouldn't it be great if I could pass these properties as args?

I wrote these constructors using the MDN Object.prototype reference and am following their example quite closely, but I've observed from reading around Stack Overflow / Code Review many examples where prototyping is used very differently, often in relation to getters and setters. This is leading me to question if I have quite grasped the 'proper' use of prototype.

Any feedback is welcome, but I am particularly interested in identifying any potential problems my approach could cause when applied to more complex situations.

\$\endgroup\$

2 Answers 2

1
\$\begingroup\$

The way you are approaching the problem is essentially correct. What you want to accomplish is also possible in a very straightforward way:

var welcomeMessage = function(messageString, xPosition, yPosition, width, height){
    this.message = messageString;
    this.x = xPosition;
    this.y = yPosition;
    this.width = width;
    this.height = height;
    this.addMessage = function(){
        STAGE.strokeText(this.message, this.x + 10, this.y + 10, this.width-10);
        console.log(this.message);
    };
};
welcomeMessage.prototype = new dialogBox();

In this way, the properties can be assigned through the constructor of the subclass as well. But we can go further. There is duplication between the constructors. We can eliminate this by calling the super constructor on the subclassed object:

var welcomeMessage = function(messageString, xPosition, yPosition, width, height){
    this.message = messageString;
    dialogBox.call(this, xPosition, yPosition, width, height);
    this.addMessage = function(){
        STAGE.strokeText(this.message, this.x + 10, this.y + 10, this.width-10);
        console.log(this.message);
    };
};
welcomeMessage.prototype = new dialogBox();

Now we have DRYed up our constructor code. This should work fine.


I just want to make a few additional points. First, rather than adding methods in the constructor, you should add them to the constructor's prototype:

dialogBox = function(...) {
    ...
}

dialogBox.prototype.draw = function() {
    ...
}

When you add methods in the constructor, they are recreated every time a new object is made, whereas if they are added to the prototype, they are created only once. Having them in the constructor also means that it has to be called to add them to a subclass. There may be reasons not to do this.

If your environment supports it, you should use Object.create(dialogBox.prototype) to set the prototype rather than new dialogBox(). This avoids calling the constructor just to set up inheritance. This is good in cases where the super class constructor does work or requires parameters be passed in.

That makes the final version:

var dialogBox = function(xPosition, yPosition, width, height){
    console.log('dialogBox Constructor');
    this.x = xPosition;
    this.y = yPosition;
    this.width = width;
    this.height = height;
};

dialogBox.prototype.draw = function() {
    STAGE.fillRect(this.x, this.y, this.width, this.height);
    STAGE.strokeRect(this.x, this.y, this.width, this.height);        
};

var welcomeMessage = function(messageString, xPosition, yPosition, width, height){
    this.message = messageString;
    dialogBox.call(this, xPosition, yPosition, width, height);
};
welcomeMessage.prototype = Object.create(dialogBox.prototype);
welcomeMessage.prototype.addMessage = function() {
    STAGE.strokeText(this.message, this.x + 10, this.y + 10, this.width - 10);
    console.log(this.message);
};

JSFiddle

As a side note, the prototype must be set before any new methods are added, so welcomeMessage.prototype = ... cannot come after you add any methods.

\$\endgroup\$
1
  • \$\begingroup\$ Thank you, this is really helpful and a very clear example of the usage of .call and Object.create. It's also very helpful that you've explained why methods are not added in the constructor itself. \$\endgroup\$
    – Candlejack
    Feb 16, 2015 at 8:50
1
\$\begingroup\$

As @cbojar pointed out, just calling dialogBox.call(this, x, y, width, height); will allow you to call the constructor on the parent class.

A few other things to point out:

  • The naming convention in JavaScript for constructor functions is to capitalize the first letter in the name, e.g. DialogBox not dialogBox. Native JavaScript constructor functions follow this same pattern (Object, Number, XMLHttpRequest, Array)
  • Furthermore, most people define the "classes" in JavaScript using named functions, e.g. function DialogBox as opposed to var DialogBox = function. The DialogBox.name property will be "DialogBox" and this name will show up in error stack traces if something in the constructor blows up
  • The constructor property of a prototype object should point to the constructor function for that "class". Be sure to set welcomeMessage.prototype.constructor = welcomeMessage to make the representation more accurate.
  • As @cbojar pointed out too, you can use Object.create now. If you must support older browsers, a pollyfill for this function is readily available by searching the web.
  • Setting the dimensions should be refactored into it's own method
  • The STAGE variable is a global variable used within the class, breaking encapsulation. Consider adding a stage property to DialogBox so you can stub out this object for unit testing
  • Be sure to set default values for x, y, width and height in the DialogBox prototype. You want to avoid drawing NaN values to your stage if at all possible and this is one way to go about it

Here would be my take on these two classes.

DialogBox

function DialogBox(x, y, width, height) {
    this.setDimensions(x, y, width, height);
    this.stage = DialogBox.stage;
}

DialogBox.stage = STAGE; // The default stage

DialogBox.prototype = {
    x: 0,
    y: 0,
    width: 400,
    height: 200,

    constructor: DialogBox,

    draw: function() {
        this.stage.fillRect(this.x, this.y, this.width, this.height);
        this.stage.strokeRect(this.x, this.y, this.width, this.height);        
    },

    setDimensions: function(x, y, width, height) {
        if (x != null)
            this.x = Number(x);

        if (y != null)
            this.y = Number(y);

        if (width != null)
            this.width = Number(width);

        if (height != null)
            this.height = Number(height);
    }
};

WelcomeMessage (Plain Old JavaScript Inheritance)

function WelcomeMessage(x, y, width, height, message) {
    DialogBox.call(this, x, y, width, height);
    this.message = message;
}

WelcomeMessage.prototype = Object.create(DialogBox.prototype);
WelcomeMessage.prototype.constructor = WelcomeMessage;

WelcomeMessage.prototype.addMessage = function() {
    this.stage.strokeText(this.message, this.x + 10, this.y + 10, this.width - 10);
};

DialogBox (With its own "extend" method for easy inheritance)

function DialogBox(x, y, width, height) {
    this.setDimensions(x, y, width, height);
    this.stage = DialogBox.stage;
}

DialogBox.stage = STAGE; // The default stage

DialogBox.extend = function(Klass, proto) {
    Klass.extend = DialogBox.extend;
    Klass.prototype = Object.create(this.prototype);
    Klass.constructor = Klass;

    if (proto) {
        for (var key in proto) {
            Klass.prototype[key] = proto[key];
        }
    }

    return Klass;
};

DialogBox.prototype = {
    x: 0,
    y: 0,
    width: 400,
    height: 200,

    constructor: DialogBox,

    draw: function() {
        this.stage.fillRect(this.x, this.y, this.width, this.height);
        this.stage.strokeRect(this.x, this.y, this.width, this.height);        
    },

    setDimensions: function(x, y, width, height) {
        if (x != null)
            this.x = Number(x);

        if (y != null)
            this.y = Number(y);

        if (width != null)
            this.width = Number(width);

        if (height != null)
            this.height = Number(height);
    }
};

WelcomeMessage (Using DialogBox.extend)

var WelcomeMessage = DialogBox.extend(
    function WelcomeMessage(x, y, width, height, message) {
        DialogBox.call(this, x, y, width, height);
        this.message = message;
    }, {
        addMessage: function() {
            this.stage.strokeText(this.message, this.x + 10, this.y + 10, this.width - 10);
        }
    }
);

Whichever inheritance scheme you choose, this is how you would use these two classes:

var box1 = new DialogBox(10, 58, 500, 300);
var box2 = new WelcomeMessage(35, 79, 650, 400, "Welcome!");

box1.draw();
box2.draw();
box2.addMessage();

Now we can tweak the WelcomeMessage class to draw the message when draw is called:

WelcomeMessage.prototype.draw = function() {
    DialogBox.prototype.draw.call(this);
    this.stage.strokeText(this.message, this.x + 10, this.y + 10, this.width - 10);
};

Or using the second inheritance scheme:

var WelcomeMessage = DialogBox.extend(
    function WelcomeMessage(x, y, width, height, message) {
        DialogBox.call(this, x, y, width, height);
        this.message = message;
    }, {
        draw: function() {
            DialogBox.prototype.draw.call(this);
            this.stage.strokeText(this.message, this.x + 10, this.y + 10, this.width - 10);
        }
    }
);

Now box2.draw() will show the message without requiring another function call.

Testing your classes

Now here's the kicker. In my refactored version, the STAGE global is replaced by a static property: DialogBox.stage. You can mock this object and unit test your classes. I'll use Jasmine tests as an example:

describe("DialogBox", function() {
    var dialog, stage;

    beforeEach(function() {
        stage = {
            fillRect: function() {},
            strokeRect: function() {},
            strokeText: function() {}
        };

        spyOn(stage, "fillRect");
        spyOn(stage, "strokeRect");
        spyOn(stage, "strokeText");
    });

    it("sets default dimensions", function() {
        dialog = new DialogBox();
        expect(dialog.x).toEqual(0);
        expect(dialog.y).toEqual(0);
        expect(dialog.width).toEqual(400);
        expect(dialog.height).toEqual(200);

    });

    describe("draw", function() {
        beforeEach(function() {
            dialog = new Dialog(10, 20, 500, 300);
            dialog.stage = stage;
        });

        it("draws the dialog on the stage", function() {
            dialog.draw();

            expect(stage.fillRect).toHaveBeenCalledWith(10, 20, 500, 300);
            expect(stage.strokeRect).toHaveBeenCalledWith(10, 20, 500, 300);
        });
    });
});
\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.