13

I've been building a small JS framework for use at my job, and I'd like to employ Douglas Crockford's prototypical inheritance patterns. I think I get the general idea of how the prototype object works, but what isn't clear is the way in which I would use this pattern beyond the simplest example.

I'll flesh it out to the point that I understand it.

(function () {

    'use strict';

    var Vehicles = {};

    Vehicles.Vehicle = function () {
        this.go = function () {
            //go forwards
        };

        this.stop = function () {
            //stop
        };
    };

    Vehicles.Airplane = Object.create(Vehicles.Vehicle());

}());

So now my Vehicles.Airplane object can go() and stop(), but I want more. I want to add takeOff() and land() methods to this object. I could just use ugly dot notation afterwards:

Vehicles.Airplane.takeOff = function () {
    //take off stuff
}

But that seems wrong, especially if I were to add many methods or properties. The question asked at here seems to be very similar to mine, but the answer doesn't quite ring true for me. The answer suggests that I should build an object literal before using Object.create, and that I should pass that object literal into the create method. In the example code given, however, it looks like their new object inherits nothing at all now.

What I'm hoping for is some syntax similar to:

Vehicles.Airplane = Object.create(Vehicles.Vehicle({
    this.takeOff = function () {
        //takeOff stuff
    };
    this.land = function () {
        //land stuff
    };
}));

I know this syntax will break terribly with Object.create right now, because of course I'm passing Vehicle.Vehicle a function rather than an object literal. That's beside the point. I'm wondering in what way I should build new properties into an object that inherits from another without having to list them out one at a time with dot notation after the fact.


EDIT:

Bergi, after some anguished thought on the topic, I think I really want to go with what you described as the "Classical Pattern". Here is my first stab at it (now with actual code snippets rather than mocked up hypotheticals - You even get to see my crappy method stubs):

CS.Button = function (o) {
    o = o || {};

    function init(self) {
        self.domNode = dce('a');
        self.text = o.text || '';
        self.displayType = 'inline-block';
        self.disabled = o.disabled || false;

        self.domNode.appendChild(ctn(self.text));
        if (o.handler) {
            self.addListener('click', function () {
                o.handler(self);
            });
        }
    }

    this.setText = function (newText) {
        if (this.domNode.firstChild) {
            this.domNode.removeChild(this.domNode.firstChild);
        }
        this.domNode.appendChild(ctn(newText));
    };

    init(this);
};
CS.Button.prototype = Object.create(CS.Displayable.prototype, {
    constructor: {value: CS.Button, configurable: true}
});

CS.Displayable = function (o) { // o = CS Object
    o = o || {};

    var f = Object.create(new CS.Element(o));

    function init(self) {
        if (!self.domAnchor) {
            self.domAnchor = self.domNode;
        }
        if (self.renderTo) {
            self.renderTo.appendChild(self.domAnchor);
        }
    }

    //Public Methods
    this.addClass = function (newClass) {
        if (typeof newClass === 'string') {
            this.domNode.className += ' ' + newClass;
        }
    };
    this.addListener = function (event, func, capture) {
        if (this.domNode.addEventListener) {
            this.domNode.addEventListener(event, func, capture);
        } else if (this.domNode.attachEvent) {
            this.domNode.attachEvent('on' + event, func);
        }
    };
    this.blur = function () {
        this.domNode.blur();
    };

    this.disable = function () {
        this.disabled = true;
    };

    this.enable = function () {
        this.disabled = false;
    };

    this.focus = function () {
        this.domNode.focus();
    };

    this.getHeight = function () {
        return this.domNode.offsetHeight;
    };

    this.getWidth = function () {
        return this.domNode.offsetWidth;
    };

    this.hide = function () {
        this.domNode.style.display = 'none';
    };

    this.isDisabled = function () {
        return this.disabled;
    };

    this.removeClass = function (classToRemove) {
        var classArray = this.domNode.className.split(' ');
        classArray.splice(classArray.indexOf(classToRemove), 1);
        this.domNode.className = classArray.join(' ');
    };

    this.removeListener = function () {
        //Remove DOM element listener
    };

    this.show = function () {
        this.domNode.style.display = this.displayType;
    };

    init(this);
};
CS.Displayable.prototype = Object.create(CS.Element.prototype, {
    constructor: {value: CS.Displayable, configurable: true}
});

I should be quite clear and say that it's not quite working yet, but mostly I'd like your opinion on whether I'm even on the right track. You mentioned "instance-specific properties and methods" in a comment in your example. Does that mean that my this.setText method and others are wrongly placed, and won't be available to descendant items on the prototype chain?

Also, when used, it seems that the order of declaration now matters (I can't access CS.Displayable.prototype, because (I think) CS.Button is listed first, and CS.Displayable is undefined at the time that I'm trying to reference it). Is that something I'll just have to man up and deal with (put things in order of ancestry in the code rather than my OCD alphabetical order) or is there something I'm overlooking there as well?

Community
  • 1
  • 1
beejay
  • 591
  • 5
  • 10
  • 2
    The second parameter of [`Object.create`](https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Object/create) takes a `propertiesObject` where you can define further properties. – Paul S. Jan 10 '13 at 21:37

2 Answers2

13
Vehicles.Airplane = Object.create(Vehicles.Vehicle());

That line is wrong. You seem to want to use new Vehicles.Vehicle - never call a constructor without new!

Still, I'm not sure which pattern you want to use. Two are coming to my mind:

Classical Pattern

You are using constructor functions just as in standard JS. Inheritance is done by inheriting the prototype objects from each other, and applying the parent constructor on child instances. Your code should then look like this:

Vehicles.Vehicle = function () {
    // instance-specific properties and methods,
    // initialising
}
Vehicles.Vehicle.prototype.go = function () {
     //go forwards
};
Vehicles.Vehicle.prototype.stop = function () {
    //stop
};

Vehicles.Airplane = function() {
    // Vehicles.Vehicle.apply(this, arguments);
    // not needed here as "Vehicle" is empty

    // maybe airplane-spefic instance initialisation
}
Vehicles.Airplane.prototype = Object.create(Vehicles.Vehicle.prototype, {
    constructor: {value:Vehicles.Airplane, configurable:true}
}); // inheriting from Vehicle prototype, and overwriting constructor property

Vehicles.Airplane.prototype.takeOff = function () {
   //take off stuff
};

// usage:
var airplane = new Vehicles.Airplace(params);

Pure Prototypical Pattern

You are using plain objects instead of constructor functions - no initialisation. To create instances, and to set up inheritance, only Object.create is used. It is like having only the prototype objects, and empty constructors. instancof does not work here. The code would look like this:

Vehicles.Vehicle = {
    go: function () {
         //go forwards
    },
    stop: function () {
         //stop
    }
}; // just an object literal

Vehicles.Airplane = Object.create(Vehicles.Vehicle); // a new object inheriting the go & stop methods
Vehicles.Airplane.takeOff = function () {
   //take off stuff
};

// usage:
var airplane = Object.create(Vehicles.Airplane);
airplane.prop = params; // maybe also an "init" function, but that seems weird to me
Robusto
  • 31,447
  • 8
  • 56
  • 77
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Thank you for the excellent overview. I imagine I'm being contradictory in my request, and for that I apologize. I want the user of my framework to be able to create an instance of one of my creations by using the "new" keyword. Example: var myAirplane = new Vehicles.Airplane({}); That said, I want the user to pass in a config object to that function, and I want the newly created item to have all of the methods of Airplane and all of the methods of Vehicle and all of the methods of whatever Vehicle inherits from as well. – beejay Jan 11 '13 at 00:19
  • As such, I suppose I'm not sure what pattern best fits my desires. I've become something of a Crockford apologist because of JSLint (it has forced my code quality to improve dramatically), so I suppose I'd like to try the purely prototypical pattern, but I don't like the idea of declaring every object method piecemeal (line 11 of your second example... Vehicles.Airplane.takeOff = function...). It feels like I should be able to declare all of those methods at the point of creation. – beejay Jan 11 '13 at 00:23
  • 2
    @user1608128 The pseudoclassical approach is even more verbose: `Vehicles.Airplane.prototype.takeOff = function...`. However I was thinking, since you expect your users to use `new`, maybe it's a good reason to use pseudoclassical yourself too? But be warned: Crockford hates the `new` keyword! :). – bfavaretto Jan 11 '13 at 01:12
  • @user1608128: If you want to use the (pseudo-)classical approach you can use my code from above. The lengthy assignments to the prototype objects can be replaced by using `extend()` functionality (copying properties) and an object literal. – Bergi Jan 11 '13 at 01:21
  • 2
    @bfavaretto: Yeah, Crockford has strong opinions, but his "hate" is unreasonable. It's true that an own operator for `Object.create` inheritance would be nice (and its lack a bad design), but `new`+constructor is still useful when you have object initialisation code. They have different aims, for further info see http://stackoverflow.com/q/2709612/1048572 or http://stackoverflow.com/q/4166616/1048572 – Bergi Jan 11 '13 at 01:29
5

You got Object.create wrong. The first argument should be an object (maybe that's why people suggested you pass a literal).

In your first example, you're actually passing undefined:

Vehicles.Airplane = Object.create(Vehicles.Vehicle()); // the function call will
                                                       // return undefined

The following would work, but it's not very Crockford-ish:

Vehicles.Airplane = Object.create(new Vehicles.Vehicle());

The way I believe Crockford would do it (or, at least, wouldn't complain of):

var Vehicles = {};

Vehicles.Vehicle = {
    go : function() {
        // go stuff
    },
    stop : function() {
        // go stuff
    }
};

Vehicles.Airplane = Object.create(Vehicles.Vehicle, {
    takeOff : { 
        value : function() {
            // take-off stuff
        }
    },
    land : {
        value: function() {
            // land stuff
        }
    }
});

Note that Vehicles.Vehicle is just a literal, which will be used as the prototype for other objects. When we call Object.create, we pass Vehicles.Vehicle as the prototype, and takeOff and land will be own properties of Vehicles.Airplane. You may then call Object.create again, passing Vehicles.Airplane as the prototype, if you want to create e.g. a Boeing.

The own properties passed as the second parameter are packed in an object that contains a representation of their property descriptors. The outer keys are the names of your properties/methods, and each one points to another object containing the actual implementation as the value. You may also include other keys like enumerable; if you don't they'll take the default values. You can read more about descriptors on the MDN page about Object.defineProperty.

bfavaretto
  • 71,580
  • 16
  • 111
  • 150
  • `Object.create` with a literal seems pointless to me. Why not just use the literal as-is? – Bergi Jan 10 '13 at 21:51
  • You mean to define `Vehicles.Vehicle`? I realized that a few minutes ago, and just updated my answer. – bfavaretto Jan 10 '13 at 21:52
  • Your second argument to `Object.create` is wrong, it expects property descriptors not plain values. You would need to combine it with a call to `extend` if you want to use a literal. (In my lib I've got an extra helper function for that :-) – Bergi Jan 10 '13 at 21:53
  • Argh, I always forget that! – bfavaretto Jan 10 '13 at 21:54
  • @Bergi I hope it's okay now. – bfavaretto Jan 10 '13 at 21:59
  • I had read up on the second argument to Object.create, but couldn't really figure how I would use it in this scenario. Bergi, tell me more about combining it "with a call to extend". My quick Google searches are returning piles of unrelated results when I include the word "extend" in my search. Is this an ECMA5 thing? – beejay Jan 10 '13 at 22:02
  • 1
    @user1608128 I added a few more details about that to my answer, I hope it helps. – bfavaretto Jan 10 '13 at 22:13
  • bfavaretto, I think your updated version of the Object.create is close to what I'm looking for. I'm going to go tinker with it some. I'll report back. – beejay Jan 11 '13 at 00:27
  • +1 vote for the great response. var Vehicles; must be var Vehicles = {}; otherwise get exceptions. – onalbi Aug 25 '14 at 11:49