JavaScript is a curious language. It's easy to write, but difficult to master. By the end of this article, hopefully, you'll transform your spaghetti code into a five-course meal, full of readable, maintainable yumminess!
Why is it So Tough?
The thing to remember, above all else when writing JS code, is that it's a dynamic language. This means there are a lot of ways to do things. You don't have to deal with strongly typed classes, or some of the more complex features from languages, like C# and Java. This is both a blessing and a curse.
The "hardness" of JavaScript is clearly evident when considering the following image:
The teeny tiny book on the left is Douglas Crockford's MUST READ book, JavaScript: The Good Parts. Towering next to it, on the right, is, JavaScript The Definitive Guide, by David Flanagan.
While both of these books are excellent reads, The Good Parts illustrates that, although JavaScript has a LOT of stuff in it, the good parts can be summed up in a considerably shorter read. So, if you're looking for a good, quick read, go with The Good Parts - and read it a few times!
This, naturally, led to a lot of sleepless nights for web developers.
You can read an article on the history of JavaScript here, but the gist of it is that Brandon Eich, in 1995, was hired by Netscape to design a language. What he came up with was the loosely typed language that we know as JavaScript. Over the years, it became "standardized" as ECMAscript, but, throughout all the browser wars, the various browsers implemented these features differently. This, naturally, lead to a lot of sleepless nights for web developers. This problem, when combined with the fact that JavaScript was considered to be most applicable for manipulating images and performing quick bits of validation, led JavaScript to, incorrectly, be viewed as a terrible language.
It's time to fix that! While, yes, there are plenty of bad things about JavaScript, when used properly, it can be a fantastic language - and it's dynamic nature will grow on you!
Making it Better
Namespaces
One of the downfalls of how JavaScript is implemented is that it operates on top of a global object. In the case of browsers, this will wind up being the window
object. So, anytime that code like this is present on a page...
function doStuff(){ alert('I am doing stuff'); } function doMoreStuff(){ var images = document.images.length; console.log("There are " + images + "on this page"); } doStuff(); doMoreStuff();
The functions doStuff
and the doMoreStuff
functions are immediately available to the global window
object.
This means that if anyone comes along and attempts to write a function, which is also called, doStuff
, there will be a conflict! All script
tags are basically taking the code within them, and running it against the window
in the order that they are referenced in the HTML. As a result, the second person to implement doStuff
will overwrite the first doStuff
.
A common technique for eliminating this problem is to take advantage of either self-executing anonymous functions, or namespaces. The object-oriented folks reading this are likely already familiar with the concept of a namespace, but the basic idea is to group functions into different areas for re-usability.
var NS = NS || {}; // "If NS is not defined, make it equal to an empty object" NS.Utils = NS.Utils || {}; NS.Models = NS.Models || {}; NS.Views = NS.Views || {};
This will prevent pollution of the global namespace, and will aid in readability for your application. Now, you simply define functions in their respective namespace. A commonly defined namespace is app
, which manages the rest of the application.
Design Patterns and Practices
In every language, there exists a set of design patterns. Addy Osmani says...
Design patterns are reusable solutions to commonly occurring problems in software design.
There are lots, and, when used correctly, they can greatly impact your application's maintainabilty. Addy wrote a great JavaScript design patterns book, called Essential Design Patterns. Absolutely give it a read!
Another commonly used pattern is the Revealing Module Pattern.
NS.App = (function () { // Initialize the application var init = function () { NS.Utils.log('Application initialized...'); }; // Return the public facing methods for the App return { init: init }; }()); NS.App.init();
Above, an App
function is defined within the NS
object. Inside, a function variable for init
is defined, and returned as an anonymous object literal. Notice that, at the end, there's that extra set of parenthesis: }());
. This forces the NS.App
function to automatically execute and return. Now, you can call NS.App.init()
to initialize your app.
The anonymous function above is a best practice in JavaScript, and is referred to as a Self-Executing Anonymous Function. Because functions in JavaScript have their own scope - i.e. variables defined inside of functions are not available outside of them - this makes anonymous functions useful in multiple ways.
// Wrap your code in a SEAF (function (global) { // Now any variables you declare in here are unavailable outside. var somethingPrivate = 'you cant get to me!'; global.somethingPublic = 'but you can however get to me!'; }(window)); console.log(window.somethingPublic); // This works... console.log(somethingPrivate); // Error
In this example, because this function is automatically executed, you can pass the window
in to the executing part }(window));
, and it will be made available as global
inside of the anonymous function. This practice limits the global variables on the window
object, and will assist in preventing naming collisions.
Now, you can start using SEAF's in other areas of your application to make the code feel more modular. This allows for your code to be re-usable, and promotes good separation of concerns.
Here's an example of a potential use for these ideas.
(function ($) { var welcomeMessage = 'Welcome to this application!' NS.Views.WelcomeScreen = function () { this.welcome = $('#welcome'); }; NS.Views.WelcomeScreen.prototype = { showWelcome: function () { this.welcome.html(welcomeMessage) .show(); } }; }(jQuery)); $(function () { NS.App.init(); }); // Modify the App.init above var init = function () { NS.Utils.log('Application initialized...'); this.welcome = new NS.Views.WelcomeScreen(); this.welcome.showWelcome(); };
So, above, there are a few different things going on. Firstly, jQuery
is passed as an argument to the anonymous function. This ensures that the $
is actually jQuery inside of the anonymous function.
Next, there's a private variable, called welcomeMessage
, and a function is assigned to NS.Views.WelcomeScreen
. Inside this function, this.welcome
is assigned to a jQuery DOM selector. This caches the selector inside the welcomeScreen
, so that jQuery doesn't have to query the DOM for it more than once.
DOM queries can be memory intensive, so please ensure that you cache them as much as possible.
Next, we wrap the App init
within $(function(){});
, which is the same thing as doing $(document).ready()
.
Finally, we add some code to the app initializer. This keeps your code nice and separated, and will be considerably easy to come back to and modify at a later day. More maintainability!
Observer Pattern
Another excellent pattern is the Observer Pattern - sometimes referred to as "Pubsub." Pubsub essentially allows us to subscribe to DOM events, such as click
and mouseover
. On one hand, we're listening to these events, and, on the other, something is publishing those events - for example, when the browser publishes (or announces) that someone clicked on a particular element. There are many libraries for pubsub, as it's a short bit of code. Perform a quick Google search, and thousands of choices will make themselves available. One solid choice is AmplifyJS's implementation.
// A data model for retrieving news. NS.Models.News = (function () { var newsUrl = '/news/' // Retrieve the news var getNews = function () { $.ajax({ url: newsUrl type: 'get', success: newsRetrieved }); }; var newsRetrieved = function (news) { // Publish the retrieval of the news amplify.publish('news-retrieved', news); }; return { getNews: getNews }; }());
This code defines a model to fetch news from some kind of service. Once the news has been retrieved with AJAX, the newsRetrieved
method fires, passing through the retrieved news to Amplify, and is published on the news-retrieved topic.
(function () { // Create a news views. NS.Views.News = function () { this.news = $('#news'); // Subscribe to the news retrieval event. amplify.subscribe('news-retrieved', $.proxy(this.showNews)); }; // Show the news when it arrives NS.Views.News.prototype.showNews = function (news) { var self = this; $.each(news, function (article) { self.append(article); }); }; }());
This code above is a view for displaying the retrieved news. In the News
constructor, Amplify subscribes to the news-retrieved topic. When that topic is published, the showNews
function is fired, accordingly. Then, the news is appended to the DOM.
// Modify this the App.init above var init = function () { NS.Utils.log('Application initialized...'); this.welcome = new NS.Views.WelcomeScreen(); this.welcome.showWelcome(); this.news = new NS.Views.News(); // Go get the news! NS.Models.News.getNews(); };
Again, modify the init
function from the app to add the news retrieval... and you're done! Now, there are separate pieces of the application, each of which is responsible for a single action. This is known as the Single Responsibility Principle.
Documentation and Files/Minification
One of the keys to maintainable code of any kind - not just JS - is documentation and commenting. Comments can serve to be invaluble for new developers coming into a project - needing to understand what's occurring in the code. "Why did I write that one line again?". An excellent tool for generating documentation is called, Docco. This is the same tool that generates the documentation for the Backbone.js web site. Basically, it takes your comments, and places them side by side with your code.
There are also tools, like JSDoc, which generate an API style documentation, describing every class in your code.
Another thing, which can prove to be difficult when starting a new project, is trying to determine how to best organize your code. One way is to separate pieces of functionality into separate folders. For example:
- /app.js
- /libs/jquery.js
- /libs/jquery-ui.js
- /users/user.js
- /views/home.js
This structure helps keep pieces of functionallity apart from one another. There are, of course, several ways to organize code, but all that really matters is deciding on a structure... and then rolling with it. Next, you can make use of a build and minification tool. There are lots of choices:
These tools will strip out whitespace, remove comments, and combine all specified files into one. This reduces the file sizes and HTTP requests for the application. Even better, this means that you can keep all of your files separated during development, but combined for production.
AMD
Asynchronous Module Definition is a different way of writing JavaScript code.
Asynchronous Module Definition is a different way of writing JavaScript code; it divides all code into separate modules. AMD creates a standard pattern for writing these modules to load in code asynchronously.
Using script
tags blocks the page, as it loads until the DOM is ready. Therefore, using something like AMD will allow the DOM to continue loading, while the scripts are also still loading. Essentially, each module is divided into its own file, and then there's one file that kicks off the process. The most popular implementation of AMD is RequireJS.
// main.js require(['libs/jquery','app.js'], function ($, app) { $(function () { app.init(); }); }); // app.js define(['libs/jquery', 'views/home'], function ($, home) { home.showWelcome(); }); // home.js define(['libs/jquery'], function ($) { var home = function () { this.home = $('#home'); }; home.prototype.showWelcome = function () { this.home.html('Welcome!'); }; return new home(); });
In the code snippet above, there is a main.js
file, which is where the process begins. The first argument to the require
function is an array of dependencies. These dependencies are a list of files that are required for app.js
. As they finish loading, whatever the module returns is passed as an argument to the function callback on the right.
Then, there is app.js
, which requires jQuery, as well as a view. Next, the view, home.js
, only requires jQuery. It has a home
function within it, and returns an instance of itself. In your application, these modules are all stored within separate files, making your application very maintainable.
Conclusion
Keeping your applications maintainable is extremely important for development. It reduces bugs, and makes the process of fixing ones that you do find easier.
"Friends don't let friends write spaghetti code!"
Comments