简体   繁体   中英

Prototypical OO in JavaScript

TL;DR:

Do we need factories/constructors in prototypical OO? Can we make a paradigm switch and drop them completely?

The BackStory:

I've been toying with doing prototypical OO in JavaScript lately and find that 99% of OO done in JavaScript is forcing classical OO patterns into it.

My take on prototypical OO is that it involves two things. A static prototype of methods (and static data) and a data binding. We don't need factories or constructors.

In JavaScript these are Object literals containing functions and Object.create .

This would mean we can model everything as a static blueprint/prototype and a data binding abstraction that's preferably hooked straight into a document-style database. Ie objects are taken out of the database and created by cloning a prototype with the data. This would mean there is no constructor logic, no factories, no new .

The Example code:

An pseudo example would be :

var Entity = Object.create(EventEmitter, {
    addComponent: {
        value: function _addComponent(component) {
            if (this[component.type] !== undefined) {
                this.removeComponent(this[component.type]);
            }

            _.each(_.functions(component), (function _bind(f) {
                component[f] = component[f].bind(this);
            }).bind(this));

            component.bindEvents();

            Object.defineProperty(this, component.type, {
                value: component,
                configurable: true
            });

            this.emit("component:add", this, component);
        }
    },
    removeComponent: {
        value: function _removeComponent(component) {
            component = component.type || component;

            delete this[component];

            this.emit("component:remove", this, component);
        }
    }
}

var entity = Object.create(Entity, toProperties(jsonStore.get(id)))

The minor explanation:

The particular code is verbose because ES5 is verbose. Entity above is a blueprint/prototype. Any actual object with data would be created by using Object.create(Entity, {...}) .

The actual data (in this case the components) is directly loaded from a JSON store and injected directly into the Object.create call. Of course a similar pattern is applied to creating components and only properties that pass Object.hasOwnProperty are stored in the database.

When an entity is created for the first time it's created with an empty {}

The actual Questions:

Now my actual questions are

  • Open source examples of JS prototypical OO?
  • Is this a good idea?
  • Is it in-line with the ideas and concepts behind prototypical OOP?
  • Will not using any constructors/factory functions bite me in the ass somewhere? Can we really get away with not using constructors. Are there any limitations using the above methodology where we would need factories to overcome them.

I don't think the constructor/factory logic is necessary at all, as long as you change how you think about Object-Oriented Programming. In my recent exploration of the topic, I've discovered that Prototypical inheritance lends itself more to defining a set of functions that use particular data. This isn't a foreign concept to those trained in classical inheritance, but the hitch is that these "parent" objects don't define the data to be operated on.

var animal = {
    walk: function()
    {
        var i = 0,
            s = '';
        for (; i < this.legs; i++)
        {
            s += 'step ';
        }

        console.log(s);
    },
    speak: function()
    {
        console.log(this.favoriteWord);
    }
}

var myLion = Object.create(animal);
myLion.legs = 4;
myLion.favoriteWord = 'woof';

So, in the above example, we create the functionality that goes along with an animal, and then create an object that has that functionality, along with the data necessary to complete the actions. This feels uncomfortable and odd to anyone who's used to classical inheritance for any length of time. It has none of the warm fuzziness of the public/private/protected hierarchy of member visibility, and I'll be the first to admit that it makes me nervous.

Also, my first instinct, when I see the above initialization of the myLion object is to create a factory for animals, so I can create lions, and tigers, and bears (oh my) with a simple function call. And, I think, that's a natural way of thinking for most programmers - the verbosity of the above code is ugly, and seems to lack elegance. I haven't decided whether that's simply due to classical training, or whether that's an actual fault of the above method.

Now, on to inheritance.

I have always understood inhertance in JavaScript to be difficult. Navigating the ins and outs of the prototype chain is not exactly clear. Until you use it with Object.create , which takes all the function-based, new-keyword redirection out of the equation.

Let's say we wanted to extend the above animal object, and make a human.

var human = Object.create(animal)
human.think = function()
{
    console.log('Hmmmm...');
}

var myHuman = Object.create(human);
myHuman.legs = 2;
myHuman.favoriteWord = 'Hello';

This creates an object which has human as a prototype, which, in turn, has animal as a prototype. Easy enough. No misdirection, no "new object with a prototype equal to the prototype of the function". Just simple prototypal inheritance. It's simple, and straightforward. Polymorphism is easy, too.

human.speak = function()
{
    console.log(this.favoriteWord + ', dudes');
}

Due to the way the prototype chain works, myHuman.speak will be found in human before it's found in animal , and thus our human is a surfer instead of just a boring old animal.

So, in conclusion ( TLDR ):

The pseudo-classical constructor functionality was kind of tacked on to JavaScript to make those programmers trained in classical OOP more comfortable. It is not , by any means, necessary, but it means giving up classical concepts such as member visibility and (tautologically) constructors.

What you get in return is flexibility, and simplicity. You can create "classes" on the fly - every object is, itself, a template for other objects. Setting values on child objects will not affect the prototype of those objects (ie if I used var child = Object.create(myHuman) , and then set child.walk = 'not yet' , animal.walk would be unaffected - really, test it).

The simplicity of inheritance is honestly mind-boggling. I've read a lot on inheritance in JavaScript, and written many lines of code attempting to understand it. But it really boils down to objects inherit from other objects . It's as simple as that, and all the new keyword does is muddle that up.

This flexibility is difficult to use to its fullest extent, and I'm sure I have yet to do it, but it's there, and it's interesting to navigate. I think most of the reason that it hasn't been used for a large project is that it simply isn't understood as well as it could be, and, IMHO, we're locked into the classical inheritance patterns we all learned when we were taught C++, Java, etc.

Edit

I think I've made a pretty good case against constructors. But my argument against factories is fuzzy.

After further contemplation, during which I've flip-flopped to both sides of the fence several times, I have come to the conclusion that factories are also unnecessary. If animal (above) were given another function initialize , it would be trivial to create and initialize a new object that inherits from animal .

var myDog = Object.create(animal);
myDog.initialize(4, 'Meow');

New object, initialized and ready for use.

@Raynos - You totally nerd sniped me on this one. I should be getting ready for 5 days of doing absolutely nothing productive.

As per your comment that the question is mainly "is constructor knowledge necessary?" I feel it is.

A toy example would be storing partial data. On a given data set in memory, when persisting I may only choose to store certain elements (either for the sake of efficiency or for data consistency purposes, eg the values are inherently useless once persisted). Let's take a session where I store the user name and the number of times they've clicked on the help button (for lack of a better example). When I persist this in my example, I do have no use for the number of clicks, since I keep it in memory now, and next time I load the data (next time the user logs in or connects or whatever) I will initialise the value from scratch (presumably to 0). This particular use case is a good candidate for constructor logic.

Aahh, but you could always just embed that in the static prototype: Object.create({name:'Bob', clicks:0}); Sure, in this case. But what if the value wasn't always 0 at first, but rather it was something that required computation. Uummmm, say, the users age in seconds (assuming we stored the name and the DOB). Again, an item that there is little use persisting, since it will need to be recalculated on retrieval anyway. So how do you store the user's age in the static prototype?

The obvious answer is constructor/initialiser logic.

There are many more scenarios, although I don't feel the idea is much related to js oop or any language in particular. The necessity for entity creation logic is inherent in the way I see computer systems model the world. Sometimes the items we store will be a simple retrieval and injection into a blueprint like prototype shell, and sometimes the values are dynamic, and will need to be initialised.

UPDATE

OK, I'm going to try for a more real-world example, and to avoid confusion assume that I have no database and need not persist any data. Let's say I'm making a solitaire server. Each new game will be (naturally) a new instance of the Game prototype. It is clear to me that their is initialiser logic required here (and lots of it):

I will, for example, need on each game instance not just a static/hard-coded deck of cards, but a randomly shuffled deck. If it were static the user would play the same game every time, which is clearly not good.

I may also have to start a timer to finish the game if the player runs out. Again, not something that can be static, since my game has a few requirements: the number of seconds is inversely related to the number of games the connected player has won so far (again, no saved info, just how many for this connection), and proportional to the difficulty of the shuffle (there is an algorithm that according to the shuffle results can determine the degree of difficulty of the game).

How do you do that with a static Object.create() ?

Example of a staticly-clonable "Type":

var MyType = {
  size: Sizes.large,
  color: Colors.blue,
  decay: function _decay() { size = Sizes.medium },
  embiggen: function _embiggen() { size = Sizes.xlarge },
  normal: function _normal() { size = Sizes.normal },
  load: function _load( dbObject ) { 
    size = dbObject.size
    color = dbObject.color 
  }
}

Now, we could clone this type elsewhere, yes? Sure, we would need to use var myType = Object.Create(MyType) , but then we're done, yes? Now we can just myType.size and that is the size of the thing. Or we could read the color, or change it, etc. We haven't created a constructor or anything, right?

If you said there's no constructor there, you're wrong. Let me show you where the constructor is:

// The following var definition is the constructor
var MyType = {
  size: Sizes.large,
  color: Colors.blue,
  decay: function _decay() { size = Sizes.medium },
  embiggen: function _embiggen() { size = Sizes.xlarge },
  normal: function _normal() { size = Sizes.normal },
  load: function _load( dbObject ) { 
    size = dbObject.size
    color = dbObject.color 
  }
}

Because we've already gone and created all the things we wanted and we've already defined everything. That's all a constructor does. So even if we only clone/use static things (which is what I see the above snippets as doing) we've still had a constructor. Just a static constructor. By defining a type, we have defined a constructor. The alternative is this model of object construction:

var MyType = {}
MyType.size = Sizes.large

But eventually you're going to want to use Object.Create(MyType) and when you do, you will have used a static object to create the target object. And then it becomes the same as the previous example.

The short answer to your question "Do we need factories/constructors in prototypical OO?" is no. Factories/Constructors serve 1 purpose only: initialize the newly created object (an instance) to a specific state.

That being said, it is often uses because some objects need initialization code of some sort.

Let's use the component-based entity code you provided. A typical entity is simply a collection of components and few properties:

var BaseEntity = Object.create({},
{
    /* Collection of all the Entity's components */
    components:
    {
        value: {}
    }

    /* Unique identifier for the entity instance */
    , id:
    {
        value: new Date().getTime()
        , configurable: false
        , enumerable: true
        , writable: false
    }

    /* Use for debugging */
    , createdTime:
    {
        value: new Date()
        , configurable: false
        , enumerable: true
        , writable: false
    }

    , removeComponent:
    {
        value: function() { /* code left out for brevity */ }
        , enumerable: true
        , writable: false
    }

    , addComponent:
    {
        value: function() { /* code left out for brevity */ }
        , enumerable: true
        , writable: false
    }
});

Now the following code will create new entities based on the 'BaseEntity'

function CreateEntity()
{
    var obj = Object.create(BaseEntity);

    //Output the resulting object's information for debugging
    console.log("[" + obj.id + "] " + obj.createdTime + "\n");

    return obj;
}

Seems straight forward enough, until you go to reference the properties:

setTimeout(CreateEntity, 1000);
setTimeout(CreateEntity, 2000);
setTimeout(CreateEntity, 3000);

outputs:

[1309449384033] Thu Jun 30 2011 11:56:24 GMT-0400 (EDT)
[1309449384033] Thu Jun 30 2011 11:56:24 GMT-0400 (EDT)
[1309449384033] Thu Jun 30 2011 11:56:24 GMT-0400 (EDT)

So why is this? The answer is simple: because of prototype based inheritance. When we created the objects, there wasn't any code to set the properties id and createdTime on the actual instance, as is normally done in constructors/factories. As a result, when the property is accessed, it pulls from the prototype chain, which ends up being a single value for all entities.

The argument to this is that the Object.create() should be passed the second parameter to set this values. My response would simply be: Isn't that basically the same as calling a constructor or using a factory? It's just another way of setting an object's state.

Now with your implementation where you treat (and rightfully so) all prototypes as a collection of static methods and properties, you do initialize the object by assigning the values of the properties to the data from a data source. It may not be using new or some type of factory, but it is initialization code.

To summarize: In JavaScript prototype OOP - new is not needed - Factories are not needed - Initialization code is usually needed, which is normally done through new , factories, or some other implementation that you don't want to admit is initializing an object

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM