0

I'm new to javascript prototypes.

In examples, prototypes are assigned in-line with the main program definition, but doing so has start-up sequence ramifications.

The following shows how I currently apply a prototype to a group of singletons. It would be nice to instead assign the prototype within the descendant class, or somewhere more visibly 'bound' to it, for clarity. (Note the panels are instantiated within the controller to enforce separation.)

Is there another location/method to accomplish this I'm overlooking? Also, am I violating any well-known styles with the current approach?

var controller = new Controller();
function Controller() {
    var panels = {
        search: SearchPanel,
        results: ResultsPanel,
        details: DetailsPanel,
        action: ActionPanel,
    };
    $.each(panels, function (i, v) {
        // THE QUESTION REFERS TO THIS FOLLOWING STATEMENT:
        v.prototype = new PanelCommon();
        panels[i] = new v();
    });
    this.publish = function (site, message) {
        $.each(panels, function (i, v) {
            if (v[site]) v[site](message);
        });
    }
    /*...*/
}
function PanelCommon() { /*...*/ }
function SearchPanel() { /*...*/ }
function ResultsPanel() { /*...*/ }
function DetailsPanel() { /*...*/ }
function ActionPanel() { /*...*/ }
shannon
  • 8,664
  • 5
  • 44
  • 74
  • In your example the PanelCommon doesn't have instance members (this.something=...) but maybe your actual code has. If it does you should have something like `PanelCommon.apply(this,arguments)` in the other Panel bodies to make sure you inherit the instance members correctly. Setting prototype with an instance of PanelCommon would cause instance specific values of PanelCommon to be shared for (for example) SearchPanel instances. I would put `var controller = new Controller();` after declaring Controller just for good measure. And finally shim and use Object.create to set prototype. – HMR Nov 16 '13 at 18:00
  • HMR, can you clarify the `constructor.apply()` recommendation you offered? Yes, I do have `this.somethings=` in my prototype dependents. Is that to replace the lost constructor execution due to `Object.create` over `new`? Also, as an aside, note the panels are singletons, so shared instances of a prototype are not an issue. – shannon Nov 16 '13 at 21:55
  • 1
    Hi Shannon, what's on the prototype is shared so it'll be shared among instances. What's in the function body as `this.something` is instance specific. If you inherit you should have at least 2 points in code to set that up. 1. Parent.apply(this,arguments) to get the instance members and re use Parent initialization code. 2. Child.prototype = Object.create(Parent.prototype) to set up the prototype (shared) members. It's explained in more detail here: http://stackoverflow.com/a/16063711/1641941 – HMR Nov 17 '13 at 01:37

3 Answers3

2

Another fit for the dynamic nature of JavaScript is the concept of Mixins or Augmentation, which are sometimes more natural than prototypical inheritance.

What do I mean by a mixin?

A "mixin" that takes an object, and injects more functionality. Basically, the idea is that we are going to take an object, and start adding behavior to it.

Consider the following mixinPanelTo() function. It'll be a function that takes a constructor and adds a common render() function to it's prototype.

var mixinPanelTo = (function() {

  var render = function() {
    // a render function that all panels share
    console.log("rendering!")
  }

  // Augment
  return function(cls) {
    cls.prototype.render = render;
  }
})();

Now that we have this, we can mix that functionality into any constructor we want:

var SearchPanel = function() {}
SearchPanel.prototype.search = function(query) {
    /* search stuff */
    this.render();
}
mixinPanelTo(SearchPanel)

Then, we should be able to

var panel = new SearchPanel()
panel.search("foo"); // "rendering!" on the console

Multiple mixins

One advantage of mixins over inheritance is a more granular control over applied functionality, and also the ability to borrow functionality from multiple parents

var mixinRender = function(cls) { /* inject render */ }
var mixinSearch = function(cls) { /* inject search */ }
var mixinInfiniteScroll = function(cls) { /* inject infinite scroll */ }

var customPanel = function() {} 
mixinRender(customPanel);
mixinSearch(customPanel);
mixinInfiniteScroll(customPanel)

This would be difficult to accomplish with prototypical inheritance. Other than trying to make a bizarre class hierarchy.

Borrowing functionality

You can also have your mixin's require functionality/configuration from your target class. For instance, lets take mixinInfinitScroll

var mixinInfiniteScroll = function(cls, fetch) {
    var page = 0;

    cls.prototype.more = function() {
        var data

        // get more results
        if(typeof fetch == "function") 
        data = fetch.call(this, ++page)
        else
        // assume a key in this
        data = this[fetch](++page)

        /* do work with data */
    }
}

And then when mixing in this functionality, we can inject specific functionality:

// by key
var Panel1 = function() { }
Panel1.prototype.fetch = function() { /* go get more results */ }
mixinInifiniteScroll(Panel1, "fetch")

// or even with a direct reference
var Panel1 = function() { }
Panel1.prototype.fetch = function() { /* go get more results */ }
mixinInifiniteScroll(Panel1, Panel1.prototype.fetch)

// or even an anonymous function
var Panel1 = function() { }
mixinInifiniteScroll(Panel1, function() { /* go get more results */ })

Overriding methods

You can also override prototype methods in mixins, which makes them quite powerful

var augmentRender = function(cls, renderFn) {
    var oldRender = cls.prototype[renderFn];

    cls.prototype[renderFn] = function() {
      /* prep */
      oldRender.apply(this, arguments);
      /* make some more changes */
    }
}

And then we can say:

var Panel = function() { }
Panel.prototype.render = function() { /* my render */ }
augmentRender(Panel, "render")

Anyway, not that there is anything wrong with prototypical inheritance, but this might give you some more ideas of different ways to solve your problem by approaching it in a different way.

J. Holmes
  • 18,466
  • 5
  • 47
  • 52
  • Great information, thank you. What is the reason for the extra closure in your first example? – shannon Nov 16 '13 at 21:16
  • 1
    To hide, and also share, the `render` function that gets added. that way each extended class gets the *same* `render` rather than new ones for each invocation of the mixin – J. Holmes Nov 16 '13 at 22:31
  • I just wanted to point out that although I've marked a different answer as *the* answer to my question, each of these is relevant to my goal, and I highly recommend you read each. – shannon Nov 17 '13 at 01:41
1

Usually prototypes are assigned immediately following the declaration of the constructor function. Also, don't forget to modify the constructor property of the newly instantiated prototype.

Sean also makes an interesting point about using Object.create, but whether or not you want to do that really depends on the contents of the PanelCommon constructor function. You also may have to shim Object.create in older browsers.

function PanelCommon() {}

function SearchPanel() {}
SearchPanel.prototype = new PanelCommon();
SearchPanel.prototype.constructor = SearchPanel;

function ResultsPanel() {}
ResultsPanel.prototype = new PanelCommon();
ResultsPanel.prototype.constructor = ResultsPanel;

function DetailsPanel() {}
DetailsPanel.prototype = new PanelCommon();
DetailsPanel.prototype.constructor = DetailsPanel;

function ActionPanel() {}
ActionPanel.prototype = new PanelCommon();
ActionPanel.prototype.constructor = ActionPanel;
Mike Edwards
  • 3,742
  • 17
  • 23
  • So Mike, the well-known practice is simply to do the above, but then respect the resulting sequence dependency by moving my dependent instantiation to the end of the code? – shannon Nov 16 '13 at 16:55
  • i.e., perhaps declare `var controller;` at the head, but regardless place `controller = new Controller();` at the tail? – shannon Nov 16 '13 at 16:57
  • 1
    hmm. Yes, Javascript is order dependent sometimes, and sometimes not. Generally you should declare things before you use them, even if it's not strictly necessary. – Mike Edwards Nov 16 '13 at 16:58
  • 1
    There is a difference between a function declaration and a function definition. `function foo() {}` is a declaration, and will be visible from the top of the file, no matter where it occurs. `var foo = function() {}` is a function expression, and the value of `foo` will be `undefined` until _after_ that line is executed. Otherwise they are equivalent. You should generally try to declare before use, so "class definitions" at the top, instantiations of the class should occur below. – Mike Edwards Nov 16 '13 at 16:59
  • I guess I just have to get over the dirty feeling from writing code into my root closure. – shannon Nov 16 '13 at 17:08
  • 2
    Well, you may want to define your types in separate modules and use a module manager like requirejs to pull them in. You don't have to do everything inline in a single file/closure. – Mike Edwards Nov 16 '13 at 17:16
  • I just wanted to point out that although I've marked this answer as *the* answer to my question, each of these is highly relevant to my goal, and I recommend you read each. – shannon Nov 17 '13 at 01:42
1

You can use Object.create - that will avoid the new SuperClass weirdness of the ES3 solution:

> SearchPanel.prototype = Object.create(PanelCommon.prototype)
> SearchPanel.prototype.constructor = SearchPanel
> new SearchPanel instanceof PanelCommon
true

This can be extracted into a very simple extends function:

function extends(cls, superClass) {
    cls.prototype = Object.create(superClass.prototype);
    cls.prototype.constructor = cls;
    return cls;
}

Which can then be used like this:

var SpecialPanel = extends(function SpecialPanel() {}, PanelCommon);
Sean Vieira
  • 155,703
  • 32
  • 311
  • 293
  • Thanks Sean. Do you know offhand if this is built-in to jQuery? I seem to recall that `$.extend()` doesn't do this. – shannon Nov 16 '13 at 17:02
  • 1
    @shannon `$.extend` copies object properties from a *source* object to a *destination* object. It has nothing to do with inheritance. – plalx Nov 16 '13 at 17:19
  • I just wanted to point out that although I've marked a different answer as *the* answer to my question, each of these is relevant to my goal, and I highly recommend you read each. – shannon Nov 17 '13 at 01:42