In this article we’re going to cover some common, and not so common smart JavaScript techniques and patterns to improve your code in both size and beauty. To follow along with this tutorial it’s important that you have a good understanding of functional programming in JS and regular expressions.
Smart JavaScript Techniques and Patterns
If you already know about this
and that
, call
, apply
, bind
and prototypes, then you’re ready to start. Otherwise here’s a few articles to get you started before we begin:
- Eloquent JavaScript: Functional Programming
- Call, Apply and Bind
- John Resig’s Learning Advanced JavaScript
- regular-expressions.info
Long Multiple Conditions
This first tip might not be obvious but it’ll help you reduce those long if
statements. The idea is to replace logical operators and comparisons with a simple regex test:
If you see yourself doing this:
if (val == 'a' || val == 'b' || val == 'c')
This can be done with regex shorter and nicer:
if (/a|b|c/.test(val))
Another case that you might have to deal with is multiple variables and the AND logical operator, for example:
if (a == 1 && b == 2 && c == 3 && d == 4 && e == 5)
When you have a very long list of variables where you want to compare their values you can create an array with the expected values and another array with the given variables and use the every
method to loop and compare. every
returns true
only if all conditions are met.
var vars = [a,b,c,d,e], vals = [1,2,3,4,5]; if (vals.every(function(val,i){ return vars[i] == val }))
This could be abstracted to be reused:
function isTrue(vars, vals) { return vals.every(function(val,i){ return vars[i] == val }); } if (isTrue([a,b,c,d,e], [1,2,3,4,5]))
For the OR logical operator case you’d use the some
method which returns true
if at least one condition is met. But it’s less common to find long OR patterns like that, it would typically be mixed with ANDs:
if (a == 1 && b == 2 || c == 3 && d == 4 || e == 5 && f == 6)
In this case the logical operator approach seems smart enough.
Text Manipulation
JavaScript’s string tools are quite limited compared to other languages. Consider the case where we have some paragraphs separated by periods and we want to capitalize the first letter of every one of them. Using loops and some string methods we’d do something like this:
var str = 'the sky is blue. apples are green.'; var para = str.split('.'), // get array of paragraphs i = 0, len = para.length, p; for (; i < len; i++) { p = para[i].trim(); // make sure there's no additional whitespace para[i] = p.substr(0,1).toUpperCase() + p.substr(1, p.length); } para = para.join('. ').trim(); // put everything back together console.log(para); //^ "The sky is blue. Apples are green."
While this works fine, the obvious smart approach here is to use regular expressions, and all of this becomes incredibly beautiful and concise:
str = str.replace(/([^.]+\.\s?)/g, function(_, para) { return para.replace(/^\w/, function(first) { return first.toUpperCase(); }); }); console.log(str); // ^ "The sky is blue. Apples are green."
Consider another usual case where you want to extract all telephone numbers from some text. Let’s suppose in our string all telephone numbers have the prefix tel:
, that way we can grab only telephone numbers and not other irrelevant numbers that the string may contain. It’s evident that regex is the best tool for this task and since there might be multiple telephone numbers we’re going to use a global regex. We could try using match
first:
var str = 'Jerry, 26 years old, tel: 555-000-0000. Joy, 19 years old, tel:777-000-0000.'; var numbers = str.match(/tel:\s?([\d-]+)/g); console.log(numbers); //=> ["tel: 555-000-0000", "tel:777-000-0000"]
But as you can see the output still contains tel:
even tough we’re using a capturing group to only get the number. The problem is that match
doesn’t capture groups in global regex, for that reason you need to use exec
and a confusing while
loop pattern that you might have seen before:
var re = /tel:\s?([\d-]+)/g, numbers = [], match; while (match = re.exec(str)) { numbers.push(match[1]); } console.log(numbers); //=> ["555-000-0000", "777-000-0000"]
That’s usually the code necessary to extract global matches, but it’s not the only option. The replace
method takes a function where the first argument is the string itself and the following arguments point to the different capturing groups. The last two arguments are trivial in this case. Knowing this we can make our code smarter:
var numbers = []; str.replace(/tel:\s?([\d\-]+)/g, function(_, number) { numbers.push(number); }); console.log(numbers); //=> ["555-000-0000", "777-000-0000"]
As long as you remeber that replace
is being used only for the purpose of looping the code becomes obvious, less confusing. We could even make this available to all strings as a new gmatch
method:
String.prototype.gmatch = function(regex) { var result = []; this.replace(regex, function() { // extract matches by removing arguments we don't need var matches = [].slice.call(arguments, 1).slice(0,-2); result.push.apply(result, matches); }); return result; }; console.log(str.gmatch(/tel:\s?([\d\-]+)/g)) //=> ["555-000-0000", "777-000-0000"]
Now that looks much nicer, proper global match right on the spot.
Generating HTML Markup
A very common case when dealing with HTML strings is creating lists, like li
, td
, option
and others. Typically you’d loop to do this:
var values = ['one', 'two', 'three', 'four', 'five']; var html = ''; for (var i = 0; i < values.length; i++) { html += '<td>'+ values[i] +'</td>'; }
That seems efficient enough but for
loops are ugly. We can make this code smarter by using the join
array method:
var html = '<td>'+ values.join('</td><td>') +'</td>';
That looks better, but what if we need the index?. In that case the map
method can be used in combination with join
.
var html = values.map(function(text, i) { return '<td>'+ text +': '+ i +'</td>' }).join('');
When this gets more complex though, we end up having to write many loops with disconnected HTML pieces so everything becomes a little bit of a mess. The solution to this problem is to use templates. There are many templating libraries out there and this is no replacement, but knowing what we know so far we could create a very simple templating helper:
function template(arr, html) { return arr.map(function(obj) { return html.join('').replace( /#\{(\w+)\}/g, function(_, match) { return obj[match]; } ); }).join(''); }
It can be used like this:
var people = [ { name: 'John', age: 25, status: 'Single' }, { name: 'Bill', age: 23, status: 'Married' }, { name: 'Mika', age: 17, status: 'Single' } ]; var html = template(people, [ '<div>', '<h1>Name: #{name}</h1>', '<h2>Age: #{age}, Status: #{status}</h2>', '</div>' ]); console.log(html); // ^ // "<div><h1>Name: John</h1><h2>Age: 25, Status: Single</h2></div>\ // <div><h1>Name: Bill</h1><h2>Age: 23, Status: Married</h2></div>\ // <div><h1>Name: Mika</h1><h2>Age: 17, Status: Single</h2></div>"
With this helper you can see how easy it is now to create content in a clear, structured manner.
Going all Functional
JavaScript is a multi-paradigm language with impressive functional programming (FP) features that often make your code more concise and compact as you’ve seen in previous examples.
All latest browsers, including IE9, have support for EcmaScript5 array and object methods. Whenever there’s a for
or for..in
loop there is an alternative solution with one of these methods.
One advantage of functional looping that might not seem so obvious is that it creates a new scope. This is particularly useful when running asynchronous code inside a loop. Imagine this case:
var arr = ['a','b','c']; for (var i = 0; i < arr.length; i++) { setTimeout(function() { console.log(arr[i] +':'+ i); }, i * 1000); }
The intention here is to log a:0..b:1..c:2
with one second in between but the result ends up being undefined:3..undefined:3..undefined:3
. The typical workaround is to create a new scope:
for (var i = 1; i <= 3; i++) { setTimeout((function(i) { console.log(arr[i] +':'+ i); }(i)), i * 1000); }
Now the output is as expected but it looks a bit confusing. Since a forEach
loop already creates a new scope things become simpler:
arr.forEach(function(v, i) { setTimeout(function() { console.log(v +':'+ i); }, i * 1000); });
When working with large amounts of data using a functional approach is a great way to improve readability. Take some data like the following:
var users = [ { name: 'Fred', age: 25, job: 'Developer', membership: 'Gold' }, { name: 'John', age: 28, job: 'Cook', membership: 'Platinum' }, { name: 'Zoe', age: 32, job: 'Make-up artist', membership: 'Silver' }, { name: 'Louise', age: 35, job: 'Cook', membership: 'Silver' } ];
With array methods you could simply filter users by any criteria:
var over30 = users.filter(function(user) { return user.age >= 30; }); var cooks = users.filter(function(user) { return user.job == 'Cook'; });
This code works and looks good enough but it seems like we’ll have to repeat this pattern again and again to extract information from our object. In order to make our code smarter this time around we’re going to make use of prototypes and some functional patterns we’ve covered before to create a very small DSL to handle our data.
What we want to able to do is something like this:
var over30 = users.where('age').is('>=30'); var cooks = users.where('job').is('Cook');
First let’s take a look at the code and then will see what’s possible with this little library. It can be challenging to follow the code if you don’t have a deep understanding of functional programming in JavaScript but hopefully the comments will make it a little bit clearer.
// An Immediately Invoked Function Expression (IIFE) // to prevent us from leaking variables to the global scope (function(win) { // Main constructor for the MyStorage object function MyStorage(data) { this.data = data; // the array containing our data this.length = this.data.length; // just a shortcut } // Shortcut to create new instances of MyStorage function stored(data) { return new MyStorage(data); } // Very basic object extender function _extend(obj, target) { for (var o in obj) target[o] = obj[o]; return target; } MyStorage.prototype = { // Private: // Update data and return a new instance. // We need to clone the objects otherwise // they would be passed as reference _new: function(result) { result = result.map(function(o) { return _extend(o, {}); }); this.length = this.data.length; return stored(result); }, // Filter the current data property with a function. // The property is passed as parameter into the 'fn' callback _filter: function(fn) { return this._new(this.data.filter(function(user, i) { return fn.call(this, user[this.prop], i); }.bind(this))); }, // Public: // Get an array with a specific prop from each object // or return the data collection otherwise get: function(prop) { if (prop) return this.map(function() { return this[prop]; }); return this.data; }, // Set the property to filter or compare to where: function(prop) { return this.prop = prop, this; }, // A shortcut for semantics and: function(prop) { return this.where(prop); }, // Compare the current property to a given condition is: function(condition) { // Filter by regular expression or string/number var regex = condition instanceof RegExp ? condition : new RegExp('^'+ condition +'$'); // Extracts the symbol in cases like '>50' and '<=10'. // 'null' and '0' are given as default values if // the string doesn't contain a symbol var symbol = (/^([<>=%]+)([\d.]+)/.exec(condition) || [0,null,0]); // Process the symbol if present if (symbol[1]) return this[symbol[1]](+symbol[2]); return this._filter(function(prop) { return regex.test(prop); }); }, '>': function(v) { return this._filter(function(p) { return p > v; }); }, '>=': function(v) { return this._filter(function(p) { return p >= v; }); }, '<': function(v) { return this._filter(function(p) { return p < v; }); }, '<=': function(v) { return this._filter(function(p) { return p <= v; }); }, '%': function(v) { return this._filter(function(p) { return p % v === 0; }); }, // Sort collection by the given function // and return a new Templee instance sort: function(fn) { return this._new(this.data.sort(function() { return fn.apply(this, [].slice.call(arguments)); }.bind(this))); }, // Filter the collection by index eq: function(index) { return this._new(this.data[index]); } }; // Curry most native methods 'forEach map slice every some reduce'.split(' ').forEach(function(method) { MyStorage.prototype[method] = function(fn) { return this.get()[method](function() { return fn.apply(arguments[0], [].slice.call(arguments)); }.bind(this)); }; }); win.stored = stored; // expose constructor to user }(window));
Using Storage is very similar to how you’d use jQuery, very intuitive:
stored(users) .where('age').is('>30') .and('job').is('Cook').forEach(function(user) { console.log(user.name); // Louise });
Imagine the possibilities of combining this with the HTML templating system we created before:
var movies = [ { title: 'Spiderman', score: 7, gross: 90e6 }, { title: 'Aliens vs Predators', score: 5 , gross: 50e6 }, { title: 'American Beauty', score: 9.5 , gross: 140e6 }, { title: '500 Days of Summer', score: 8.5 , gross: 75e6 }, { title: 'Drive', score: 7.5 , gross: 120e6 }, { title: '127 Hours', score: 9 , gross: 78e6 } ]; var myMovies = stored(movies); var featured = template(myMovies.where('score').is('>=8').get(), [ '<div class="featured-movie">', '<h2>#{title}</h2>', '<h3>Score: #{score}, Gross: #{gross}</h3>', '</div>' ]); var bigGross = template(myMovies.where('gross').is('>80000000').get(), [ '<ul class="big-gross-movies">', '<li>#{title} <span class="gross">#{gross}</span></li>', '</ul>' ]); $('body').append([ '<h1>Featured Movies:</h1>'+ featured, '<h1>Big Gross Movies:</h1>'+ bigGross, ].join(''));
Other more complex examples are also possible:
// Get all movie titles that start by number myMovies.where('title').is(/^\d/).get('title'); //=> ["127 Hours", "500 Days of Summer"] // Chaining myMovies.where('gross').is('>100000000') .and('score').is('>9').get('title') //=> ["American Beauty"]
And of course you can chain all those useful native array methods, forEach
, map
, slice
, every
, some
, reduce
:
// Will log one movie title per second myMovies.forEach(function() { setTimeout(function() { console.log(this.title) }.bind(this), 1000); });
You can play with this nice demo putting these snippets together: JS Bin
Conclusion
By using functional techniques we can achieve very clean abstracted modular code. Libraries like jQuery and Zepto make use of some of these patterns to make their code smarter, better.
In terms of support, since EcmaScript5 is very well supported nowadays, all of these snippets should work in IE9 and all other modern browsers. To support older browsers you might want to use polyfills to add that functionality.
Hopefully you got some new fresh ideas to try out and make your code smarter and more efficient. Leave me a comment below if you have any questions.