Javascript Management

Best Practices

n00bz0rs

and I have extensive experience writing *computer code*, including but not limited to: OIC, BTW, BRB, IMHO, LMAO, ROFL, TTYL...

The Basics

Including Files

<!doctype html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <link rel="stylesheet" href="css/style.css"> <script src="modernizr.2.0.6.min.js"></script> </head> <body> <header role="banner"> </header> <section id="main" role="main"> </div> <footer role="content info"></footer> <script src="//ajax.googleapis.com/ajax/libs/jquery/1.6/jquery.js"></script> <script> window.jQuery || document.write('<script src="jquery.1.6.min.js">\x3C/script>')</script> <script src="script.js"></script> </body> </html>

Function Scope

var myName = 'Morten', yourName = 'ANUGs'; function switchNames(name1, name2) { var temp = name1; name1 = name2; name2 = temp; } switchNames(myName, yourName);
// myName => Morten // yourName => ANUGs // temp, name1, name2 => undefined
function switchNamesAgain() { temp = myName; myName = yourName; yourName = temp; } switchNamesAgain(); // myName => ANUGs // yourName => Morten
// temp => Morten
// this => DOMWindow undefined = true; notThere === undefined // => false

Closure With Anonymous Functions

// IIFE - Immediately-Invoked Function Expression (function () { // All vars and functions are in this scope only // Still maintains access to all globals })();
// Importing globals (function ( $, undefined ) { // The native undefined is triggered // $ is properly assigned })( jQuery );
// this => DOMWindow

Namespacing & Modules

Department of Redundancy Department

Simple Namespacing With Object Literals

// Ensure namespace var WEBAPP = WEBAPP || {};
(function ( $, undefined ) { WEBAPP.Properties = { selectedCssClass : 'sel', validationErrorCssClass : 'error' } WEBAPP.Handlers = { init : function(){ $( '#siteSearch' ).bind( 'keyup', WEBAPP.Navigation.search ) } } WEBAPP.Navigation = { search : function( e ){ var $input = $( this ), searchTerm = $input.val(); WEBAPP.Utils.evalKey( e ) === 'RETURN' && location.hash = searchTerm; } } WEBAPP.Utils = { evalKey: function( e ) { var KEY = []; KEY[ 13 ] = 'RETURN'; return KEY[ e.keyCode ? e.keyCode : e.which ]; } } $( WEBAPP.Handlers.init ); //=> $( document ).ready(); })( jQuery || ender );

Modules With Private and Public Members

// Ensure namespace var WEBAPP = WEBAPP || {};
WEBAPP.Module = (function ( $, undefined ) { // Privates var privateVar = 5, privateMethod = function () { return 'Private Test'; }; return { // Publics publicVar: 10, publicMethod: function () { return ' Followed By Public Test '; }, // With acces to private members getData: function () { return privateMethod() + this.publicMethod() + privateVar; } } })( jQuery || ender ); WEBAPP.Module.getData(); //=> 'Private Test Followed By Public Test 5'

Augmented Modules

// Augmentation WEBAPP.Module = (function ( M, $, undefined ) { M.anotherMethod = function () { return 'Augmented method'; }; return M; })( WEBAPP.Module, jQuery || ender ); WEBAPP.Module.anotherMethod(); //=> 'Augmented method'
// Loose Augmentation WEBAPP.Module = (function ( M, $, undefined ) { M.anotherMethod = function () { return 'Loosely augmented method'; }; return M; })( WEBAPP.Module || {}, jQuery || ender ); WEBAPP.Module.anotherMethod() //=> 'Loosely augmented method'

Augmented Asyncronous Modules

var WEBAPP = WEBAPP || {}; // Ensure namespace WEBAPP.Module = (function ( M, $, undefined ) { // Augmentation loads first return $.extend( M, { anotherMethod : function () { return 'Loosely augmented method'; } }); })( WEBAPP.Module || {}, jQuery || ender ); WEBAPP.Module = (function ( M, $, undefined ) { // Module afterwards var privateVar = 5, privateMethod = function () { return 'Private Test'; }; return $.extend( M, { publicVar : 10, publicMethod : function () { return ' Followed By Public Test '; }, getData : function () { return privateMethod() + this.publicMethod() + privateVar; } }); })( WEBAPP.Module || {}, jQuery || ender ); WEBAPP.Module.anotherMethod(); //=> Loosely augmented method WEBAPP.Module.getData(); //=> Private Test Followed By Public Test 5

Get More Out of jQuery

Full Potential?

Typical AJAX

// Typical ASP.NET AJAX request $.ajax({ data: JSON.stringify( { command : {} } ), url: '/services/service.svc/methodName', type: 'post', contentType: 'application/json; charset=utf-8', success : function( result ){ var res = JSON.parse( result ); res.d; //=> What we actually want }, error : function( error ){ // Do something with error } }); // Get's tedious!

Extended AJAX - Converters & Prefilters

// AJAX setup with converter $.ajaxSetup({ type : 'post', contentType: 'application/json; charset=utf-8', dataType: 'dotnetjson', converters: { // New to 1.5 "json dotnetjson": function( response ) { return response.hasOwnProperty( 'd' ) ? response.d : response; } }, error : function( error ){ // Do something with error } });
// AJAX prefilter - New to 1.5 $.ajaxPrefilter( 'dotnetjson', function( options, originalOptions ){ options.data = JSON.stringify( originalOptions.data ); options.url = '/services/service.svc/' + options.url; });
// Typical ASP.NET AJAX request is now $.when($.ajax({ data: { command : {} }, url: 'methodName' })) .done(function (result) { result //=> Is exactly what we want it to be };

Animation Configuration

// Animation speeds $.fx.speeds; //=> { _default: 400, fast: 200, slow: 600 }
// Browser specfic animation speeds WEBAPP.Properties = { animationSpeeds: (function( is ) { var factor = is.msie && is.version <= 7 ? 2 : 1; // Older IE browsers are sped up return { normal: 400 / factor, fast: 200 / factor, slow: 600 / factor } })( $.browser ) }
// Are merged into the FX object $.extend( $.fx.speeds, WEBAPP.Properties.animationSpeeds ); // And default is set $.fx.speeds._default = $.fx.speeds.normal;
// Now we can use $( '.element' ).fadeIn(); //=> 400 ms ( 200 ms for older IEs ) $( '.element' ).fadeIn( 'fast' ); //=> 200 ms ( 100 ms for older IEs )

Pluginization

$.fn; //=> $.prototype
// Simple plugin $.fn.myPlugin = function () { this.addClass('myPluginClass'); } $('#element').myPlugin(); $('#element').hasClass('myPluginClass')); //=> true
// Simple plugin supporting chaining $.fn.myPlugin = function () { this.addClass('myPluginClass'); return this; } $('#element') .myPlugin() .hasClass('myPluginClass')); //=> true
// And even simpler $.fn.myPlugin = function () { return this.addClass('myPluginClass'); } $('#element') .myPlugin() .hasClass('myPluginClass')); //=> true

Pluginization in combination with our namespace

(function ( $, undefined ) { WEBAPP.Handlers = { init : function(){ $( '#main' ) // Keep main wrapper and chain .find( 'table' ) .zebraTable() .end() .find( '.teasers' ) .equalizeHeights(); .end(); } } WEBAPP.Utils = { setZebraTable: function( tables ) { tables.each(function(){ $( this ).find( 'tr:nth-child(2n)' ).addClass( WEBAPP.Properties.rowClass ); }); } } $.fn.extend({ zebraTable : function() { WEBAPP.Utils.setZebraTable( this ); return this; }, equalizeHeights : function() { }, reverse : [].reverse //<= Lets steal it from native Array }); $( WEBAPP.Handlers.init ); })( jQuery || ender );

Low Coupling with pub/sub

(function ( $, undefined ) { WEBAPP.Handlers = { init : function(){ $.subscribe( '/navigation/addToBasket/success', WEBAPP.Navigation.updateMiniBasket ); $.subscribe( '/navigation/addToBasket/success', WEBAPP.Navigation.updateOrderCount ); $.subscribe( '/navigation/specialOrder', WEBAPP.Navigation.notifySpecial ); $( '.addToBasket' ).click( WEBAPP.Navigation.addToBasket ); } } WEBAPP.Navigation = { addToBasket : function () { $.when($.ajax({ url: 'addToBasket', data: { command : {} } })) .done( function( data ) { $.publish('/navigation/addToBasket/success', [ data.lineItems ]); data.specialOrder && $.publish('/navigation/specialOrder', []); }); } } $( WEBAPP.Handlers.init ); })( jQuery || ender );

Performance

Death by a Thousand Paper Cuts

Event binding

// Datagrid with 500 rows $( '#dataGrid a.edit' ).click( WEBAPP.Navigation.editRow );
// Assigning 500 handlers in a jiffy is BAD // So don't assign 500 handlers in a jiffy
// Much better $( '#dataGrid' ).click( function( e ){ var $target = $( e.target ); $target.is( 'a.edit' ) && WEBAPP.Navigation.editRow( e, $target ); }); // 1 binding catching events when they bubble up // Makes it work on a dynamic DOM too
// Delegate is an abstraction of the same $( '#dataGrid' ).delegate( 'a.edit', 'click', WEBAPP.Navigation.editRow );
// But it can be too late to cancel the bubbling e.stopPropagation(); //=> Has already bubbled all the way up!
// Intercept bubbling on a child element $( '#dataGrid' ).find( 'tbody' ).delegate( 'a.edit', 'click', function( e ){ specialCase && e.stopPropagation(); });

DOM Manipulation

// Datagrid with 500 rows var $grid = $( '#dataGrid' );
// Highlight all edit buttons $grid.find( 'a.edit' ).addClass( 'highLight' );
// Accesses layout properties for each element and results in 500 reflows
// Instead we create a Document Fragment var $gridFragment = $grid.detach(); //=> 1 reflow
// And do it off-the-DOM $gridFragment.find( 'a.edit' ).addClass( 'highLight' ); //=> 0 reflows
// Then append the Document Fragment again $( 'body' ).append( $gridFragment ); //=> 1 reflow
// Thus saving 498 reflows. Yay!

Sizzle Selector Performance

// FAST: ID & element selectors $( '#element, form, input' );
// Backed by .getElementById() and .getElementsByTagName()
// SLOWER: Class selectors $( '.element' );
// Backed by .getElementsByClassName() except for IE5-8
// SLOWEST: Pseudo & attribute selectors $( ':visible, :hidden' ); $( '[ attribute=value ]' );
// Backed only in latest browsers by .querySelector() and .querySelectorAll()
// Its good practice to create a context and keep it $( '#main' ) .find( '.element' ) .show(); .end() .find( '.anotherElement' ) .hide() .end();

Webservices Need Love Too

// Spamming a service every time input changes $( input.amount ).change( function(){ $.when($.ajax({ url: 'updateBasket', data: { command : {} } })) .done( function( data ) { $.publish('/navigation/updateBasket/success', []); }); });
// Instead we set the call up as a subscription $.subscribe( '/navigation/updateBasket', function(){ $.when($.ajax({ url: 'updateBasket', data: { command : {} } })) .done( function( data ) { $.publish('/navigation/updateBasket/success', []); }); });
// And publish that event after a short idle $( input.amount ).change( function(){ $.doTimeout( timerId, 500, function(){ $.publish('/navigation/updateBasket', []); }, true); });

Other Scenarios

Frameworks, Loaders and Dependancy Management

  • Require.js ( loader, dep. management )
  • LAB.js ( loader, dep. management )
  • YEPNOPE ( conditional loader )
  • Knockout.js ( framework )
  • Backbone.js ( framework )
  • Spine.js ( framework )
  • WebWorkers ( HTML5 )

VS2010 as Javascript Editor

Enhancements for VS2010

/// <reference path="jquery-1.6.2-vsdoc.js" /> $; //=> We're on a roll (function ( $ ) { $; //=> Intellisense lost in closure :| })( jQuery );

Questions?

About Me