Let's Make a Framework: Patterns
Welcome to part 44 of Let’s Make a Framework, the ongoing series about building a JavaScript framework.
If you haven’t been following along, these articles are tagged with lmaf. The project we’re creating is called Turing.
Over the last few weeks we’ve built a test framework based on the CommonJS assert module, a suitable test runner, and started converting Turing’s tests to use it. The test framework is called Turing Test.
Patterns
This week I want to discuss the patterns we’ve used to create the framework. I realise a lot of new readers find the prospect of reading a 44 part tutorial series daunting, so I thought I’d go back and explore some of the important patterns that you can use in your own JavaScript code.
Anonymous Functions
Anonymous functions are often used as callbacks for API calls:
var a = 1;
MyAPI.method('parameter', function() {
console.log(a);
});
Because this is a closure the a variable is visible to the anonymous function.
Another important use of anonymous functions is to control scope. This is used throughout our framework to create self-contained modules that only expose a small part of their functionality, thereby creating private scopes:
(function(global) {
function privateFunction() {
console.log('Hello from privateFunction');
}
var MyAPI = {
method: function() {
privateFunction();
}
};
global.MyAPI = MyAPI;
})(window);
MyAPI.method();
// Hello from privateFunction
CommonJS Module Compatibility
The previous example expects window to be defined. It can be made CommonJS module compatible like this:
function getGlobal() {
if (typeof window !== 'undefined') {
return window;
} else if (typeof exports !== 'undefined') {
return exports;
}
}
(function(global) {
function privateFunction() {
console.log('Hello from privateFunction');
}
var MyAPI = {
method: function() {
privateFunction();
}
};
global.MyAPI = MyAPI;
})(getGlobal() || this);
Now in a CommonJS-compatible interpreter this should work:
> var MyAPI = require('./test').MyAPI;
> MyAPI.method();
Many predominantly browser-based libraries are now adding CommonJS module compatibility so people can reuse them in server-side projects (Underscore and Backbone are good examples of this).
Iterators
To get around poor iterator support in some browsers, we had to build our own enumerable module. This is all built from the each function, which jQuery defines in its core. We built each like this:
turing.enumerable = {
Break: {},
each: function(enumerable, callback, context) {
try {
if (Array.prototype.forEach && enumerable.forEach === Array.prototype.forEach) {
enumerable.forEach(callback, context);
} else if (turing.isNumber(enumerable.length)) {
for (var i = 0, l = enumerable.length; i < l; i++) callback.call(enumerable, enumerable[i], i, enumerable);
} else {
for (var key in enumerable) {
if (hasOwnProperty.call(enumerable, key)) callback.call(context, enumerable[key], key, enumerable);
}
}
} catch(e) {
if (e != turing.enumerable.Break) throw e;
}
return enumerable;
},
// etc.
This approach is used by many JavaScript frameworks, and works like this:
Array.prototype.forEachis checked to see if we can use the existing functionality based into most interpreters and browsers- Else if the passed-in collection has a length, use a for loop to repeatedly call each value
- If the collection is not an Array, use a
for ... in ...loop instead - If
Breakhas been thrown by a callback, catch it and return the collection
Chained APIs
We’re used to chained APIs from jQuery (and now a growing number of JavaScript libraries):
$('selector')
.attr('attr', 'value')
.css({ 'attr', 'value' })
.find('selector')
.css({ 'attr', 'value' })
This is a useful technique because it cuts down the noise that a lot of callbacks would introduce. It’s expressive and easy to get the hang of.
We built this in turing.enumerable.js by defining a Chainer class that collects results, and a method that wraps each chainable call into a form that returns this after each method has been called.
Returning this and collecting results is how jQuery works. Because our module was designed to return results, the chained API style is triggered by calling chain first:
turing.enumerable.chain([1, 2, 3, 4])
.filter(function(n) { return n % 2 == 0; })
.map(function(n) { return n * 10; })
.values();
This is similar to Underscore.
Prototypal Classes
I’ve used JavaScript’s native prototypal classes in Turing a lot, rather than relying on a library that adds classical object-oriented features. Prototypal classes are easy to get the hang of, and they’re a quick way to organise your code:
function MyClass() {
this.instanceVar = 'a dull dude';
}
MyClass.classMethod = function() {
console.log('Greetings from MyClass');
};
MyClass.prototype.method = function() {
console.log('All work and no play makes Alex', this.instanceVar);
};
MyClass.classMethod();
var myClass = new MyClass();
myClass.method();
At first it seems strange using the function keyword to define what appears to be a class, but once you’ve got used to prototypes it becomes a powerful code organisation tool.
Prototypal Inheritance
In JavaScript 1.8.5, Mozilla introduced Object.create which can be used to inherit from objects:
function User() {
}
User.prototype.toString = function() {
return 'User';
};
User.prototype.state = function() {
return 'Logged in';
}
function Admin() {
}
// Some browsers won't have Object.create,
// this is a simple version
if (typeof Object.create !== 'function') {
Object.create = function(o) {
function F() {}
F.prototype = o;
return new F();
};
}
Admin.prototype = Object.create(User.prototype);
Admin.prototype.toString = function() {
return 'Admin';
}
var user = new User(),
admin = new Admin();
console.log('User:', user);
console.log('Admin:', admin);
console.log(user.state());
console.log(admin.state());
This example uses Crockford’s Object.create in browsers and interpreters that don’t have Object.create. It creates two prototypal classes, User and Admin, then extends Admin’s prototype to include the methods in User.
