A Curated Tour of Awesome JavaScript Sources:

 

Backbone.js Edition

Gaurav Dadhania

@GVRV

A confession

I'm not a JavaScript developer!

a confession... continued

... but, I'm excited by JavaScript!

Anything that can be written in JavaScript, will be written in JavaScript!

...But we have a problem

Often we're just writing glue code to make Library X work with Framework Z

Library X and Framework Z have a half-life in hours

Vanilla.js is often forgotten :(

It's an exciting little, elegant language that used to excite you, remember?

turn that frown, upside down!

Not against new libraries and frameworks released!

It's great we have a vibrant community and open-source led ecosystem

Reading source from such projects will help us nail the fundamentals

Learn about the language from the collective wisdom of thousands of developers!

Why Backbone.js?

May not be in fashion anymore, but still might learn a thing or two

Simple — 1600 LoC with comments!

Popular — still in use by many heavyweights and very active, helpful community!

Tested — 230 contributors, 1500+ watchers, huge amount of collective knowledge!

Initialization Pattern

Use factory function to deal with different dependency management solutions


(function(root, factory) {
  // handle different sort of dependency management solutions
}(this, function(root, Backbone, _, $) {
  // add stuff to Backbone
  return Backbone;
}));

Use IIFEs for proper encapsulation and avoid polluting global scope

Dependency Management

​// Set up Backbone appropriately for the environment. Start with AMD.
if (typeof define === 'function' && define.amd) {

  define(['underscore', 'jquery', 'exports'], function(_, $, exports) {
    // Export global even in AMD case in case this script is loaded with
    // others that may still expect a global Backbone.
    root.Backbone = factory(root, exports, _, $);
  });

Six lines to remove major friction from developers to get started with your library!

// Next for Node.js or CommonJS. jQuery may not be needed as a module.
} else if (typeof exports !== 'undefined') {

  var _ = require('underscore');
  factory(root, exports, _);
// Finally, as a browser global.
} else {
  root.Backbone = factory(
    root, 
    {}, 
    root._, 
    (root.jQuery || root.Zepto || root.ender || root.$)
  );
}
// Regular expression used to split event strings.
var eventSplitter = /\s+/;
// Implement fancy features of the Events API such as multiple event
// names `"change blur"` and jQuery-style event maps `{change: action}`
// in terms of the existing API.
var eventsApi = function(obj, action, name, rest) {
  if (!name) return true;

Backbone Events - convenience Notation

>> eventsApi({}, 'on', 'change blur', ...)
false
>> eventsApi({}, 'on', {'change': '...', 'blur': '...', ...)
false
>> eventsApi({}, 'on', 'change', ...)
true
  // Handle event maps.
  if (typeof name === 'object') {
    for (var key in name) {
      obj[action].apply(obj, [key, name[key]].concat(rest));
    }
    return false;
  }
  // Handle space separated event names.
  if (eventSplitter.test(name)) {
    var names = name.split(eventSplitter);
    for (var i = 0, l = names.length; i < l; i++) {
      obj[action].apply(obj, [names[i]].concat(rest));
    }
    return false;
  }
  return true;
};

Backbone events - On

// Bind an event to a `callback` function. Passing `"all"` will bind
// the callback to all events fired.
on: function(name, callback, context) {
  if (!eventsApi(this, 'on', name, [callback, context]) || !callback) return this;
  this._events || (this._events = {});
  var events = this._events[name] || (this._events[name] = []);
  events.push({callback: callback, context: context, ctx: context || this});
  return this;
}

Very simple, very easy to follow

// let someObj = {}; 
>> someObj.on('change all', function () { console.log('yaayyy!') }, null);
{
  '_events': {
    'change': [{
      'callback': function () { console.log('yaayyy!'); },
      'context': null,
      'ctx': someObj
    }],
    'all': [{
      'callback': function () { console.log('yaayyy!'); },
      'context': null,
      'ctx': someObj
    }]
  }
}

backbone events - trigger

// Trigger one or many events, firing all bound callbacks. Callbacks are
// passed the same arguments as `trigger` is, apart from the event name
// (unless you're listening on `"all"`, which will cause your callback to
// receive the true name of the event as the first argument).
trigger: function(name) {
  if (!this._events) return this;
  var args = slice.call(arguments, 1);
  if (!eventsApi(this, 'trigger', name, args)) return this;
  var events = this._events[name];
  var allEvents = this._events.all;
  if (events) triggerEvents(events, args);
  if (allEvents) triggerEvents(allEvents, arguments);
  return this;
}
// back to someObj
>> someObj.trigger('change', 'some', 'other', 'args');

// will result in two function calls
// one for the 'change' event 
triggerEvents([{
  'callback': function() { console.log('yaayyy!'); }, 
  'context': null, 
  'ctx': someObj 
  }], ['some', 'other', 'args']); 
// one for the 'all' event 
triggerEvents([{
  'callback': function() { console.log('yaayyy!'); }, 
  'context': null, 
  'ctx': someObj 
  }], ['change', 'some', 'other', 'args']); 

backbone events - trigger events

// A difficult-to-believe, but optimized internal dispatch function for
// triggering events. Tries to keep the usual cases speedy (most internal
// Backbone events have 3 arguments).
var triggerEvents = function(events, args) {
  var ev, i = -1, l = events.length, a1 = args[0], a2 = args[1], a3 = args[2];
  switch (args.length) {
    case 0: while (++i < l) (ev = events[i]).callback.call(ev.ctx); return;
    case 1: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1); return;
    case 2: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2); return;
    case 3: while (++i < l) (ev = events[i]).callback.call(ev.ctx, a1, a2, a3); return;
    default: while (++i < l) (ev = events[i]).callback.apply(ev.ctx, args); return;
  }
};

Don't be afraid to write ugly looking code 

Back it up using solid benchmarks though!

http://jsperf.com/events-vs-events2/33

// Remove one or many callbacks. If `context` is null, removes all
// callbacks with that function. If `callback` is null, removes all
// callbacks for the event. If `name` is null, removes all bound
// callbacks for all events.
off: function(name, callback, context) { // eg name = "change click"
  var retain, ev, events, names, i, l, j, k;
  if (!this._events || !eventsApi(this, 'off', name, [callback, context])) return this;

BaCKBONE EVENTS - off

  if (!name && !callback && !context) { this._events = void 0; return this; }
  names = name ? [name] : _.keys(this._events);
  for (i = 0, l = names.length; i < l; i++) {
    name = names[i]; //eg: name = "change"
    if (events = this._events[name]) {
      this._events[name] = retain = [];
      if (callback || context) {
        for (j = 0, k = events.length; j < k; j++) {
          ev = events[j];
          if ((callback && 
               callback !== ev.callback && 
               callback !== ev.callback._callback  /* for the 'once' convenience function */
             ) || (context && context !== ev.context)) {
            retain.push(ev);
          }
        }
      }
      if (!retain.length) delete this._events[name];
    }
  }
  return this;
}

backbone events - convenience functions

var listenMethods = {listenTo: 'on', listenToOnce: 'once'};

// Inversion-of-control versions of `on` and `once`. Tell *this* object to
// listen to an event in another object ... keeping track of what it's
// listening to.
_.each(listenMethods, function(implementation, method) {
  Events[method] = function(obj, name, callback) {
    var listeningTo = this._listeningTo || (this._listeningTo = {});
    var id = obj._listenId || (obj._listenId = _.uniqueId('l'));
    listeningTo[id] = obj;
    if (!callback && typeof name === 'object') callback = this;
    obj[implementation](name, callback, this);
    return this;
  };
});
// let a = {} and b = {} 
// then a.on('change blur', b.callback, b) ~ b.listenTo(a, 'change blur', b.callback) 
// Aliases for backwards compatibility.
Events.bind   = Events.on;
Events.unbind = Events.off;

backbone events - mixin

// Allow the `Backbone` object to serve as a global event bus, for folks who
// want global "pubsub" in a convenient place.
_.extend(Backbone, Events);

_.extend(Model.prototype, Events, { ... });
_.extend(Collection.prototype, Events, { ... });
_.extend(View.prototype, Events, { ... });
_.extend(Router.prototype, Events, { ... });
_.extend(History.prototype, Events, { ... }); 

Write mix-ins that can add functionality to your classes

Make sure your mix-ins can be easily tested!

Think hard about the abstractions your mix-ins can provide — Backbone events is just 165 LoC

Would be nearly 1000 LoC if not properly abstracted!

A detour — underscore's Extend

// Extend a given object with all the properties in passed-in object(s).
_.extend = function(obj) {
  if (!_.isObject(obj)) return obj;
  var source, prop;
  for (var i = 1, length = arguments.length; i < length; i++) {
    source = arguments[i];
    for (prop in source) {
      if (hasOwnProperty.call(source, prop)) { 
        obj[prop] = source[prop]; 
      }
    }
  }
  return obj;
};

Simple extend pattern for mix-ins

Psst... A tour of underscore source is also worth it!

>> var a = {};
>> var b = {'one': 1, 'two': 2 };
>> var c = {'two:: 'TWO', 'three': 'THREE' };

>> _.extend(a, b, c);
{
    'one': 1,
    'two': 'TWO',
    'three': 'THREE'
}

backbone - underscore augmentation

// Underscore methods that we want to implement on the Model.
var modelMethods = ['keys', 'values', 'pairs', 'invert', 'pick', 'omit'];

// Mix in each Underscore method as a proxy to `Model#attributes`.
_.each(modelMethods, function(method) {
  Model.prototype[method] = function() {
    var args = slice.call(arguments);
    args.unshift(this.attributes);
    return _[method].apply(_, args);
  };
});
// Underscore methods that we want to implement on the Collection.
// 90% of the core usefulness of Backbone Collections is actually implemented
// right here:
var methods = ['forEach', 'each', 'map', 'collect', 'reduce', 'foldl',
  'inject', 'reduceRight', 'foldr', 'find', 'detect', 'filter', 'select',
  'reject', 'every', 'all', 'some', 'any', 'include', 'contains', 'invoke',
  'max', 'min', 'toArray', 'size', 'first', 'head', 'take', 'initial', 'rest',
  'tail', 'drop', 'last', 'without', 'difference', 'indexOf', 'shuffle',
  'lastIndexOf', 'isEmpty', 'chain', 'sample'];

// Mix in each Underscore method as a proxy to `Collection#models`.
_.each(methods, function(method) {
  Collection.prototype[method] = function() {
    var args = slice.call(arguments);
    args.unshift(this.models);
    return _[method].apply(_, args);
  };
});

backbone router

// Cached regular expressions for matching named param parts and splatted
// parts of route strings.
var optionalParam = /\((.*?)\)/g;
var namedParam    = /(\(\?)?:\w+/g;
var splatParam    = /\*\w+/g;
var escapeRegExp  = /[\-{}\[\]+?.,\\\^$|#\s]/g;

// Convert a route string into a regular expression, suitable for matching
// against the current location hash.
_routeToRegExp: function(route) {
  route = route.replace(escapeRegExp, '\\$&')
               .replace(optionalParam, '(?:$1)?')
               .replace(namedParam, function(match, optional) {
                 return optional ? match : '([^/?]+)';
               })
               .replace(splatParam, '([^?]*?)');
  return new RegExp('^' + route + '(?:\\?([\\s\\S]*))?$');
}
>> var route = '(!/):code/shop-now(/)(:department)(/)(:category)(/)?*query';
>> _routeToRegExp(route)
/^(?:!/)?([^/?]+)/shop\-now(?:/)?(?:([^/?]+))?(?:/)?
(?:([^/?]+))?(?:/)?\?([^?]*?)(?:\?([\s\S]*))?$/

Yes, I had to split the regex into 2 lines

>> var route = "(foo/):country/shop-now(/)(:department)(/)(:category)(/)?*query";
// characters that have meaning in regexes
>> var escapeRegExp = /[\-{}\[\]+?.,\\\^$|#\s]/g;

// $& is the JS regex last matched group 
>> route.replace(escapeRegExp, '\\$&')  
"(foo/):country/shop\-now(/)(:department)(/)(:category)(/)\?*query"
// things like "(any character, non-greedy)" and match characters
// within () 
>> var optionalParam = /\((.*?)\)/g; 
// make it non-remembering group (?:) and optional using suffix '?' 
>> route.replace(optionalParam, '(?:$1)?')
"(?:foo/)?:country/shop\-now(?:/)?(?::department)?(?:/)?(?::category)?(?:/)?\?*query"
// Match starting "(?" optionally and then ":" followed by any word
>> var namedParam = /(\(\?)?:\w+/g;
// convert things like "(?::department)?" to "(?:([^/?]+))?" but leave things like
// (?:foo)? alone 
>> route.replace(namedParam, function(match, optional) {
     return optional ? match : '([^/?]+)';
   })
"(?:foo/)?([^/?]+)/shop\-now(?:/)?(?:([^/?]+))?(?:/)?(?:([^/?]+))?(?:/)?\?*query"
// Match the splat character 
>> var splatParam    = /\*\w+/g;
// Non-greedily match all characters that don't have a starting '?' character
>> route.replace(splatParam, '([^?]*?)')
"(?:foo/)?([^/?]+)/shop\-now(?:/)?(?:([^/?]+))?(?:/)?(?:([^/?]+))?(?:/)?\?([^?]*?)"
// add the matching for the query string after the '?' character
>> route + '(?:\\?([\\s\\S]*))?$' 

backbone Router

// Given a route, and a URL fragment that it matches, return the array of
// extracted decoded parameters. Empty or unmatched parameters will be
// treated as `null` to normalize cross-browser behavior.
_extractParameters: function(route, fragment) {
  var params = route.exec(fragment).slice(1); // cause '0' index is the matched string
  return _.map(params, function(param, i) {
    // Don't decode the search params.
    if (i === params.length - 1) return param || null;
    return param ? decodeURIComponent(param) : null;
  });
}
// regex of route "(something/):country/shop-now(/):department(/)(:category)(/)?*query"
>> _extractParameters(
     /^(?:something/)?([^/?]+)/shop\-now(?:/)?
    ([^/?]+)(?:/)?(?:([^/?]+))?(?:/)?\?([^?]*?)(?:\?([\s\S]*))?$/,
     "something/foo/shop-now/televisions/led-tvs/?utm_campaign=newsletter"
   )
["foo", "televisions", "led-tvs", "utm_campaign=newsletter", null]

// Yes, I still had to break down the regex into 2 lines!

    ​backbone router,

// Manually bind a single named route to a callback. For example:
//
//     this.route('search/:query/p:num', 'search', function(query, num) {
//       ...
//     });
//
route: function(route, name, callback) {
  if (!_.isRegExp(route)) route = this._routeToRegExp(route);
// Backbone.History
// Add a route to be tested when the fragment changes. Routes added later
// may override previous routes.
route: function(route, callback) {
  this.handlers.unshift({route: route, callback: callback});
}
  if (_.isFunction(name)) {
    callback = name;
    name = '';
  }
  if (!callback) callback = this[name];
  var router = this;
  Backbone.history.route(route, function(fragment) {
    var args = router._extractParameters(route, fragment);
    router.execute(callback, args);
    router.trigger.apply(router, ['route:' + name].concat(args));
    router.trigger('route', name, args);
    Backbone.history.trigger('route', router, name, args);
  });
  return this;
}

backbone history

// Save a fragment into the hash history, or replace the URL state if the
// 'replace' option is passed. You are responsible for properly URL-encoding
// the fragment in advance.
// The options object can contain `trigger: true` if you wish to have the
// route callback be fired (not usually desirable), or `replace: true`, if
// you wish to modify the current URL without adding an entry to the history.
navigate: function(fragment, options) {
  if (!History.started) return false;
  if (!options || options === true) options = {trigger: !!options};
  // this.getFragment strips leading '#' or '/' and trailing '/' 
  var url = this.root + (fragment = this.getFragment(fragment || ''));
  // Strip the trailing hash for matching
  fragment = fragment.replace(pathStripper, '');
  if (this.fragment === fragment) return;
  this.fragment = fragment;
  // Don't include a trailing slash on the root.
  if (fragment === '' && url !== '/') url = url.slice(0, -1);
  // If pushState is available, we use it to set the fragment as a real URL.
  this.history[options.replace ? 'replaceState' : 'pushState']({}, document.title, url);
  // If hash changes haven't been explicitly disabled, update the hash
  // fragment to store history.
  if (options.trigger) return this.loadUrl(fragment);
}

backbone history

// Attempt to load the current URL fragment. If a route succeeds with a
// match, returns `true`. If no defined routes matches the fragment,
// returns `false`.
loadUrl: function(fragment) {
  fragment = this.fragment = this.getFragment(fragment);
  // this.handlers are all the routes we pushed to the router!
  return _.any(this.handlers, function(handler) {
    if (handler.route.test(fragment)) {
      handler.callback(fragment);
      return true;
    }
  });
}
// Checks the current URL to see if it has changed, and if it has,
// calls `loadUrl`, normalizing across the hidden iframe.
checkUrl: function(e) {
  var current = this.getFragment();
  if (current === this.fragment && this.iframe) {
    current = this.getFragment(this.getHash(this.iframe));
  }
  if (current === this.fragment) return false;
  if (this.iframe) this.navigate(current);
  this.loadUrl();
}

backbone router + history

Specify routes that are converted to RegExp and stored on both Router and History

Hijack a click event and use Backbone.History.navigate and pass the new URL

Backbone.History.navigate will get the fragment, see if it has changed and use browser history API to change state

It might even call Backbone.History.loadUrl which goes through all routes and checks for a match

If a match is found, the route handler is called and we have our SinglePageApp*!

*Adjust for unknown corner cases and cross browser issues, of course

Backbone extend

How do I implement classical inheritance in JavaScript?!

 

Easy to understand classical and prototypal inheritance on paper, but often stumped when asked how to implement classical inheritance in code

// Helper function to correctly set up the prototype chain, for subclasses.
// Similar to `goog.inherits`, but uses a hash of prototype properties and
// class properties to be extended.
var extend = function(protoProps, staticProps) {
  var parent = this;
  var child;
  // The constructor function for the new subclass is either defined by you
  // (the "constructor" property in your `extend` definition), or defaulted
  // by us to simply call the parent's constructor.  
  if (protoProps && _.has(protoProps, 'constructor')) {
    child = protoProps.constructor;
  } else {
    child = function(){ return parent.apply(this, arguments); };
  }
  // Add static properties to the constructor function, if supplied.
  _.extend(child, parent, staticProps);
  // Set the prototype chain to inherit from `parent`, without calling
  // `parent`'s constructor function.
  var Surrogate = function(){ this.constructor = child; };
  Surrogate.prototype = parent.prototype;
  child.prototype = new Surrogate;
  // Add prototype properties (instance properties) to the subclass,
  // if supplied.
  if (protoProps) _.extend(child.prototype, protoProps);
  // Set a convenience property in case the parent's prototype is needed
  // later.
  child.__super__ = parent.prototype;

  return child;
};
>> function ParentClass (id) { console.log('My id is', id); this.init(); }
>> ParentClass.prototype.init = function () { console.log('In parent class init!'); }
>> ParentClass.classname = "Parent"; 
>> var p = new ParentClass(123); 
My id is 123
In parent class init!

>> Object.getPrototypeOf(p).constructor.classname 
Parent
>> ParentClass.extend = extend; 
>> var ChildClass = ParentClass.extend({
     init: function () { console.log('In child class init!'); }
   }, {
     classname: 'Child'
   });
>> var c = new ChildClass(123);
My id is 123 // same constructor as parent, can be overloaded though! 
In child class init! // overridden 'init' function from child 

>> Object.getPrototypeOf(c).constructor.classname
Child 
>> Object.getPrototypeOf(c).constructor.__super__
{
  'constructor': function (id) { console.log('My id is', id); this.init(); }
  'init': function () { console.log('In parent class init!'); }
}

>> Object.getPrototypeOf(c).constructor.__super__.constructor.classname
Parent

IN closing

Vanilla.js is awesome!

Pick your favorite library or framework today and view source!

Share what you learn or what surprises you!

thank you!

A Curated Tour of Awesome JavaScript Sources:

By gvrv

A Curated Tour of Awesome JavaScript Sources:

  • 1,310