-
-
Notifications
You must be signed in to change notification settings - Fork 353
JavaScript Coding Standards
The core feature set of Rock's front-end functionality depend heavily upon a few common JavaScript libraries. These will be included on every page:
We seek to minimize dependence on third party libraries. We will be maintaining these and are committing to staying on a recent version of each library. In the interest of performance, all of these libraries will be bundled together and minified, and included in the DOM via ~/bundles/RockLibs
.
bundles.Add( new ScriptBundle( "~/bundles/RockLibs" ).Include(
"~/Scripts/jquery.js",
"~/Scripts/jquery-ui-1.10.0.custom.min.js",
"~/Scripts/Kendo/kendo.web.min.js",
"~/Scripts/bootstrap.min.js",
"~/Scripts/bootbox.min.js",
"~/Scripts/jquery.tinyscrollbar.js" ) );
If you're developing a block, and you need to register a JavaScript on the page in a way that will ensure that it does not get added more than once, we've exposed a method call on the CurrentPage
object that will allow you to register your external JS file without worrying about collisions.
protected void OnLoad( EventArgs e )
{
base.OnLoad( e );
this.CurrentPage.AddScriptLink( this.Page, "~/Scripts/path-to-your-file.js" );
}
Under the covers, this method uses ASP.NET's built in ScriptManager
control to handle registering all JavaScript with the current page, and will ensure the same script file does not get added to the DOM more than once.
We try to adhere pretty closely to Doug Crockford's JS style guide. We use var
in front of all of our declarations, manually hoist our variables, and try to never use eval
, etc.
Brace placement, casing, etc
In our JavaScript code, the Rock team uses the K&R brace placement style. We use camelCasing
for all variable declarations, and PascalCasing
for "class" declarations. That style is pretty standard throughout the JavaScript community, and we believe that you shouldn't be able to tell what a developer's favorite server stack is by looking at their JavaScript.
Manual variable hoisting
The current version of JavaScript supported in modern browsers does not have block-level scope. So variables declared within code blocks, like for
loops or if
blocks are visible outside of that scope.
if (true) {
var a = 3;
}
// This will actually output '3' to the console.
console.log(a);
JavaScript has lexical (or function) scope. A variable will be available within the scope of the function it was declared, and any child functions declared within that scope.
To avoid any confusion it's considered good practice to manually hoist all of your variable declarations to the top of the function in which it was declared.
function foo () {
var a = 3,
b = function () {
var c = 0,
i;
for (i = 0; i < 5; i++) {
c += i;
}
return c;
};
console.log(a + b()); // > 7
console.log(c); // > undefined
}
Equality checks and type coercion
When checking for equality of values, it's best to use the triple equals (===
) notation, rather than double equals (==
). When using double equals on values of two disparate types, JavaScript will attempt to cast the second operand to the first one's type in order to determine whether that statement is truthy or falsy.
// Do this...
if (a === b) {
// ...
}
// And this...
if (a !== b) {
// ...
}
// Not this...
if (a == b) {
// ...
}
// Or this...
if (a != b) {
// ...
}
The reasoning is that triple equals will not attempt to use type coercion to evaluate the values being compared. If you're comparing two disparate types, they will remain two disparate types.
Here are a few examples of why double equals can be dangerous:
'' == 0
// > true
'' === 0
// > false
0 == false
// > true
0 === false
// > false
'1' == 1
// > true
'1' === 1
// > false
undefined == null
// > true
undefined === null
// > false
When checking the truthiness of a value, and you don't know what type it might be (e.g. - it could be an object, or a string), it's best to test the truthiness of the variable itself, rather than comparing it to multiple values:
// Do this...
if (foo) {
//...
}
// And this...
var a = foo ? 1 : 0;
// Not this...
if (foo !== null && (typeof foo === 'string' || typeof foo === 'object')) {
// ...
}
// Or this...
var a = (foo !== null && (typeof foo === 'string' || typeof foo === 'object')) ? 1 : 0;
When testing the existence of a member or an object, it's considered best practice to check for its type, rather than checking whether or not it's null:
// Consider this case...
var foo = {
bar: function () {
console.log('hi there!');
}
};
if (someCondition) {
foo.bar = 'baz';
}
// Do this...
if (typeof foo.bar === 'function') {
// This is very durable. It will never throw an error.
foo.bar();
}
// Not this...
if (foo.bar !== null) {
// If foo.bar === 'baz', the next line will throw an error.
foo.bar();
}
You will also run into logical operators being used in expressions. They can be used to evaluate the truthiness of the operand. You'll see the or logical OR operator (||
) used in similar fashion to C#'s null coalescing operator (??
).
// If window.Rock is not `undefined`, `null`, `false`, `0` or `''`, window.Rock will be assigned.
// Otherwise, Rock will be assigned the value of a new object.
var Rock = window.Rock || {};
Strict mode
It's considered good practice to put your development code into strict mode. To do this, include the 'use strict';
pragma at the top of your outermost function closure in all of your JS files. This will put constraints on what the browser will allow you to do. The browser will now perform handy JIT-time error checking on your code, which includes things like throwing an error if you forget a var
keyword in front of a variable declaration or define duplicate properties on an object.
Within Rock's client API's we make heavy use of the Module Pattern. You can think of a JavaScript "module" as being similar to a Singleton. However, since JavaScript in the browser is currently single-threaded, there's no need to worry as much about concurrency, locking, etc. Here's an example of the anatomy of a JavaScript module as we use it in Rock:
// We wrap our entire code block in an anonymous, self-calling function, or "closure".
// You will often see global variables passed in to ensure their durability, and encapsulate
// them within the closure. It's important to remember that all code defined within the
// closure is inaccessible from outside. This helps keep each module neatly encapsulated.
(function ($) {
// Make sure to put your code into strict mode...
'use strict';
// Establishing proper namespacing. This helps minimize collisions in the global scope.
window.Rock = window.Rock || {};
Rock.someNamespace = Rock.someNamespace || {};
// And here's our module. Notice it's another self-calling anonymous function.
// Whatever our anonymous function returns will be accessible from the global
// scope like so: `var foos = Rock.someNamespace.someModule.findFoos(5);`
Rock.someNamespace.someModule = (function () {
// This is a private function. It is not accessible from outside the module,
// but can be freely accessed from inside.
var _findFoos = function (num) {
var foos = [],
i;
for (i = 0; i < num; i++) {
foos.push('foo_' + (i + 1));
}
return foos;
},
// Here's our return value, enabling our module to have an external interface.
// Naming our return value exports is a convention we've taken from the Common.js
// module standard.
exports = {
findFoos: function (num) {
return _findFoos(num);
}
};
return exports;
}());
}(jQuery));
JavaScript doesn't do inheritance the way most "reasonable" languages do. Rather than class
es and classical inheritance, JavaScript uses the object prototype chain to achieve the same behavior.
// This may seem like a normal function, but depending on how it is invoked, it will
// create an instance of itself. If we use the `new` keyword, we can consider this
// function to be a constructor.
var Animal = function (name) {
// By using `this` here, we're adding a property to the function's prototype
// that will be accessible on an instance of this function.
this.name = name;
};
// You can create methods on an object by adding functions to its prototype
Animal.prototype.speak = function () {
return 'blergh!';
};
// If we invoke this function with the `new` operator, we get an instance of it, with
// access to its prototype.
var george = new Animal('George');
console.log(george.name); // > 'George'
console.log(george.speak()); // > 'blergh!'
var Dinosaur = function (name) {
this.name = name;
};
// We can have our `Dinosaur` "class" inherit from `Animal` by assigning its prototype
Dinosaur.prototype = Animal;
// We can now call the `speak` method on a Dinosaur...
var dino = new Dinosaur();
console.log(dino.speak()); // > 'blergh!'
// And we can override methods of the "super class" simply by defining them on the "base class"
Dinosaur.prototype.speak = function () {
return 'roar!'
};
var animal = new Animal();
console.log(animal.speak()); // > 'blergh!'
var dinosaur = new Dinosaur();
console.log(dinosaur.speak()); // > 'roar!'
Using instances of objects paired with the module pattern can become an incredibly powerful way of managing instances of functionality on the same page.
(function () {
'use strict';
window.Rock = window.Rock || {};
Rock.someModule = (function () {
// We can define a class as a private member of a module
var SomeClass = function (options) {
this.id = options.id
},
exports;
SomeClass.prototype.doSomething = function () {
console.log('processing #' + this.id);
};
exports = {
cache: {},
init: function (options) {
// And whenever the module is initialized by outside code, it can
// create an instance of an object that encapsulates all necessary
// functionality, and cache it for later if needed (this can be
// very handy for storing the results of ajax requests, history, etc).
var someObj = new SomeClass(options);
exports.cache[options.id] = someObj;
someObj.doSomething();
}
};
return exports;
}());
}());
Because JavaScript is a dynamically typed, functional* language, it has a bit of a wonky treatment of the context of this
. Since JavaScript's function
construct is actually a first class citizen with its own prototype, it has a notion of "calling context" and its own set of methods (e.g. - call
, apply
, etc).
var foo = function (str) {
console.log(str);
};
// When you invoke a function this way...
foo('foo');
// It's actually shorthand for this...
foo.call(window, 'foo');
// If the object is invoked as a member of the global scope, or is
// not tied to an object literal or prototype, `this` will default
// to `window`.
// Now if we add our function to an object...
var bar = {
foo: foo
}
// And we call it like this...
bar.foo('baz');
// It's actually shorthand for this...
bar.foo.call(bar, 'baz');
This technique can be leveraged very powerfully once you get your mind wrapped around it. We're all probably familiar with how the jQuery team has leveraged this feature of JavaScript in a very useful way.
// In a jQuery event handler, `this` is the DOM element that triggered
// the event. So by wrapping it in a jQuery `$()` call, you get all of the
// magic of jQuery at your disposal.
$('#some-button').click(function () {
$(this).next().toggle();
});
// Under the covers, the way the jQuery team implemented it probably looks
// something like this:
var handleEvent = function (id, fn, eventArgs) {
var el = document.getElementByID(id);
fn.call(el, eventArgs);
};
But when you combine some of these concepts, it can get pretty confusing.
(function () {
'use strict';
var Rock = window.Rock || {};
Rock.someModule = (function () {
var SomeClass = function (options) {
this.id = options.id;
this.name = options.name;
},
exports;
SomeClass.prototype.init = function () {
// Out here `this` is the current instance of `SomeClass`
// in scope. It can be helpful to stash `this` in a variable
// in an outer scope, so you can refer to it from within
// an inner scope.
var that = this;
$('#' + that.id).click(function () {
// In here, `this` is the DOM element that triggered the event
// handler. It's very helpful (and more performant) to stash
// that jQuery return value in a variable if you're going to refer
// to it more than once inside your event handlers.
var $el = $(this);
$el.siblings('input:hidden').val(that.id);
if ($el.next().is(':hidden')) {
$el.next().show().text(that.name);
}
});
};
exports = {
init: function (options) {
var someObj = new SomeClass(options);
someObj.init();
}
};
}());
}());
// And here's the usage of our neatly encapsulated module.
Rock.someModule.init({ 1, 'Foo' });
There are some libraries that can help clear up the confusion. Libraries like Underscore have helper functions built in that will bind all the methods of an object in a predictable manner. This can help non-JS developers have a better handle on what's going on in their code, thereby lessening the amount of hair needlessly pulled out in frustration.
*Under the hood, JavaScript actually has more in common with LISP than it does with C/C++, yet many developers assume the opposite, because it looks so familiar.
If you're looking for a really solid overview of all the important stuff, I highly recommend reading Doug Crockford's JavaScript: The Good Parts. This is widely considered to be the definitive book on understanding the basics of JS, the common "gotchas" and how to write JS well.
If you want to dive deeper, Addy Osmani has a great (open source) book on JS Design Patterns: Learning JavaScript Design Patterns.