Skip to content

Instantly share code, notes, and snippets.

@mapsam
Created January 20, 2015 00:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mapsam/75afc9142f18d909850f to your computer and use it in GitHub Desktop.
Save mapsam/75afc9142f18d909850f to your computer and use it in GitHub Desktop.
scope question for @thebigspoon

So for this random project (another one) I'm working on geocodeing a spreadsheet of addresses and adding them to a map. I'm building with the constructor.prototype stuff that you showed me with Guss and am wondering how far I can extend the prototype for more organization. Say for instance I'd like to split my functions into categories such as "spreadsheet" and "map" where I could essentially do all spreadsheet functions by calling:

this.spreadsheet.get OR this.spreadsheet.query OR this.spreadsheet.print_to_dom

and similarly could use my mapping functions like:

this.map.init_tiles OR this.map.add_points

basically keeping everything scoped within the same set of parent functions instead of all at the same parent constructor level.

Is the best way to to do this by essentially making that first-level prototype function a standard object like:

constructor.prototype.map = {
  init_tiles: function () { ... },
  add_points: function () { ... },
  etc: function etc () { ... }
}

??

It doesn't seem to work by declaring the object literally in stride like constructor.map.add_points because using this.map.add_points within the scope results in an undefined error. Is this where I should start using constructor.Class or something?

@ranchodeluxe
Copy link

Ooooh Yeah!! This is a good tricky question homeslice. I think the only way to achieve this ( it would be nice to know if there are other ways too ) is to forcibly call all nested map or spreadsheet scoped functions by passing the context. You can do this using the dynamic fn.call( context ) for no ( or single args ) or fn.apply( context, [ args ] ) for many args. For example, here is your class:

var Bam = function( options ) {
    this.options = options || {};
    this.name = 'BAM';
};


Bam.prototype.map = {
        init_tiles: function () { console.log( 'init_tiles: ', this.spreadsheet.foo.call( this ) ) },
        add_points: function () { console.log( 'add_points: ', this.map.etc.call( this ) ) },
        etc: function etc () { console.log( 'etc: ', this.spreadsheet.bar.call( this ) ) }
};

Bam.prototype.spreadsheet = {
        foo: function () { console.log( 'foo: ', this.options ); return this.options },
        bar: function () { console.log( 'bar: ', this.options ); return this.options },
        baz: function etc () { console.log( 'baz: ', this.options ); return this.options }
};

Now, assuming that's loaded in the browser you could do the follow where >>> is the output on the console:

var bammer = new Bam({ 'hoot' : 'toot' });
bammer.map.init_tiles.call( bammer );
>>> foo:  { hoot: 'toot' }
>>> init_tiles:  { hoot: 'toot' }
bammer.map.add_points.call( bammer );
>>> bar:  { hoot: 'toot' }
>>> etc:  { hoot: 'toot' }
>>> add_points:  undefined
bammer.map.etc.call( bammer );
>>> bar:  { hoot: 'toot' }
>>> etc:  { hoot: 'toot' }
bammer.spreadsheet.foo.call( bammer );
>>> foo:  { hoot: 'toot' }
bammer.spreadsheet.bar.call( bammer );
>>> bar:  { hoot: 'toot' }
bammer.spreadsheet.baz.call( bammer );
>>> baz:  { hoot: 'toot' }

@ranchodeluxe
Copy link

you mention something about constructor.Class. Where did you get that from? Is it something new in ES6?

@mapsam
Copy link
Author

mapsam commented Jan 20, 2015

I think my confusion came from the leaflet-src.js file, which has an actual object named Class but the syntax highlighter made it seem like it was something reserved for javascript or something. Where does using prototype come in, when I could just not use the word at all? Is this more of an organizational usage rather than purely functional?

/*
 * L.Class powers the OOP facilities of the library.
 * Thanks to John Resig and Dean Edwards for inspiration!
 */

L.Class = function () {};

L.Class.extend = function (props) {

    // extended class with the new prototype
    var NewClass = function () {

        // call the constructor
        if (this.initialize) {
            this.initialize.apply(this, arguments);
        }

        // call all constructor hooks
        if (this._initHooks) {
            this.callInitHooks();
        }
    };

    // instantiate class without calling constructor
    var F = function () {};
    F.prototype = this.prototype;

    var proto = new F();
    proto.constructor = NewClass;

    NewClass.prototype = proto;

    //inherit parent's statics
    for (var i in this) {
        if (this.hasOwnProperty(i) && i !== 'prototype') {
            NewClass[i] = this[i];
        }
    }

    // mix static properties into the class
    if (props.statics) {
        L.extend(NewClass, props.statics);
        delete props.statics;
    }

    // mix includes into the prototype
    if (props.includes) {
        L.Util.extend.apply(null, [proto].concat(props.includes));
        delete props.includes;
    }

    // merge options
    if (props.options && proto.options) {
        props.options = L.extend({}, proto.options, props.options);
    }

    // mix given properties into the prototype
    L.extend(proto, props);

    proto._initHooks = [];

    var parent = this;
    // jshint camelcase: false
    NewClass.__super__ = parent.prototype;

    // add method for calling all hooks
    proto.callInitHooks = function () {

        if (this._initHooksCalled) { return; }

        if (parent.prototype.callInitHooks) {
            parent.prototype.callInitHooks.call(this);
        }

        this._initHooksCalled = true;

        for (var i = 0, len = proto._initHooks.length; i < len; i++) {
            proto._initHooks[i].call(this);
        }
    };

    return NewClass;
};

@ranchodeluxe
Copy link

A couple things:

1)

There's no reason you have to use the keyword prototype to build your class. For example, you could create a Bam class like the snippet below. Both the way it's done above and below are mostly identical. There are subtle differences about where the attributes are stored. It's mostly a matter of 1) preference and slightly 2) code reuse for setting up inheritance. Word of advice, just choose one and stick with it like a pattern ( that's basically why i build everything off the prototype ). Number #2 below explains more about the subtle differences.

var Bam = function( options ) {

    this.options = options || {};
    this.name = 'BAM';
    this.map = {
        init_tiles: function () { console.log( 'init_tiles: ', this.spreadsheet.foo.call( this ) ) },
        add_points: function () { console.log( 'add_points: ', this.map.etc.call( this ) ) },
        etc: function etc () { console.log( 'etc: ', this.spreadsheet.bar.call( this ) ) }
    };
    this.spreadsheet = {
        foo: function () { console.log( 'foo: ', this.options ); return this.options },
        bar: function () { console.log( 'bar: ', this.options ); return this.options },
        baz: function etc () { console.log( 'baz: ', this.options ); return this.options }
    };

};

2)

I think one thing to focus on is that prototype has a special relationship with a constructor function.

For example, given this:

var Class = function(options){
    this.options = options || {};
    this.name = 'FUCK YEAH';
}

What do we expect the variable x to hold after executing this code?

var x = Class();  

and what do we expect the variable 'x' to hold here?

var x = new Class();  

The difference between a normal function and a constructor function is the use of the keyword new. The first example above is just a normal function and since nothing is returned, it returns undefined:

var x = Class();  
>>> x
>>> undefined

The second example above uses the keyword new. In cases like these, the function does some magic which you should be familiar with and always returns a fresh object. Since one of those magic steps is binding a prototype object to the new object, then let's illustrate what x would hold and where it holds it when using prototype:

// assuming this code
var Class = function(options){
    this.options = options || {};
    this.name = 'FUCK YEAH';
}
Class.prototype.just_the_tip = true;
Class.prototype.fn = function(){ console.log( 'fn' ); };
var x = new Class();

/*  
** only properties and functions declared inside the constructor function will live
** on the first-level object represented by 'x'
** we test this out using the 'hasOwnProperty' function which will return
** false if the property lives up the prototype chain
*/
for ( var k in x ){ if( x.hasOwnProperty( k ) ){ console.log( k ); } }
>>> options 
>>> name 

/*  
** all prototype properties will live on the prototype object that 
** hangs off the first-level object represented by 'x'
*/
x.constructor.prototype
>>> Object {fn: function, just_the_tip: true}


/*
** however, we don't need to care where they live
** because anything in prototype is accessible
** through the 'x' dot operator just as if it lived
** on the first level object
*/
x.fn
>>> function
x.just_the_tip
>>> true

I'm glad you're reading the Leaflet code and used it here. That function above L.Class.extend has everything to do with reusing the prototype to mimic inheritance. Notice all the proto stuff in that code. Many projects use this same style of extend to do inheritance. For example, Rancho uses a very similar function that was stolen from Backbone.

With generic extending functions like that, you can now dynamically create prototype chains by passing objects when doing inheritance. Here is an example on Rancho

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment