As we all know, Javascript is a very flexible language. In the article I will show different ways to execute conditional code by using some common idioms from Javascript and general object-oriented techniques.
Default values
Javascript does not support default values for arguments and it is common to use an if statement or a conditional expression to set default values.
function swim(direction, speed, technique) { // Default value with if statement if (!direction) direction = 'downstream'; // Default value with conditional operator var speedInMph = speed ? speed : 2; }
I usually prefer to use an or
-expression instead. The short-circuiting or,
||
, avoids the repetition of the conditional operator and is, in my opinion,
more readable. Another advantage of avoiding repetition is that a slow
executing function condition such as fastestSwimmer()
will avoid the
performance penalty of calling the function twice.
// Default value with or. var swimTechnique = technique || 'crawl'; // Function is only invoked once var siwmmer = fastestSwimmer() || 'Michael Phelps'; }
Naturally, this technique is not limited to default arguments, it can be used to set default values from object literals too.
options = { kind: 'Mountain Tapir', } var kind = options['kind'] || 'Baird Tapir';
A simple, yet useful, technique.
Update 2013-04-13, as Jeffery mentions in a comment, the technique
only works for values that are not falsy. If values such as 0
or false
are
acceptable values, you will have to explicitly test for undefined
instead.
Call callback if present
Another idiom in Javascript, especially in Node, is passing callbacks to other
functions. But, sometimes the callbacks past in are optional. In this case we
can use short-circuiting and, &&
, instead.
function updateStatistics(data, callback) { var result = doSomethingWithData(data); // Call the callback if it is defined if (callback) return callback(result); } function (data, callback) { var result = doSomethingWithData(data); // The last evaluated value of the `&&` is returned return callback && callback(result); }
I, personally, prefer the first form with the explicit if
because I think it
communicates my intent better but, it is good to know about the technique
anyway.
Update 2013-04-13, A better use for the technique is for testing for the presence of objects before getting their properties.
function callService(url, options) { ajaxCall(url, options && options.callback); }
Lookup Tables
If you have code that behaves differently based on the value of a property, it
can often result in conditional statements with multiple else if
s or a
switch
cases.
if (kind === 'baird') bairdBehavior(); else if (kind === 'malayan') malayanBehavior(); else if (kind === 'mountain') mountainBehavior(); else if (kind === 'lowland') lowlandBehavior(); else throw new Error('Invalid kind ' + kind);
I find this kind of code ugly and I don't think it looks any better with a
switch
statement. I prefer to use a lookup table if there is more than two
options.
var kinds = { baird: bairdBehavior, malayan: malayanBehavior, mountain: mountainBehavior, lowland: lowlandBehavior }; var func = kinds[kind]; if (!func) throw new Error('Invalid kind ' + kind); func();
I find this code a lot clearer since makes it clear that the else
clause
handles an exceptional case and that the normal cases works similarly.
Missing Objects
If similar conditionals appear in multiple places in my code, it is a sign that I am missing an object somewhere. Since Javascript is duck typed I can use the same technique as above to create objects instead of just functions.
var kinds = { baird: { act: bairdBehavior, info: bairdInfo }, malayan: { act: malayanBehavior, info: malayanInfo }, mountain: { act: mountainBehavior, info: mountainInfo }, lowland: { act: lowlandBehavior, info: lowlandInfo }, }; var tapir = kinds[kind]; if (!tapir) throw new Error('Invalid kind of tapir ' + kind);
I prefer to have this kind of code on the borders of my application. That way the code inside my core domain doesn't have to deal with complicated conditional logic. Polymorphism for the win!
Null Objects
If I notice that in many places I have to check fornull
s, it is usually a
sign that I haven't handled the special null
case properly. In the example
above I have handled it properly since I throw an Error
if the kind of tapir
does not exist. But sometimes it is not an error when the value is missing.
// If a non existant kind is used, tapir will become null var tapir = kinds[kind]; // In other places of the code if (tapir) tapir.act(); // Somewhere else if (tapir) return tapir.info();
This type of code is rather unpleasant and it is time to break out the Null
Object
.
var tapir = kinds[kind]; // If a non existant kind is used I use a Null Object if (!tapir) tapir = { act: doNothing, info: unknownTapirInfo } // In other places of the code the conditionals are gone. tapir.act(); // Somewhere else, no special case here. return tapir.info();
Null Objects
are not appropriate everywhere but, I often find it very
enlightening to have them in mind when I write code.
Summary
There are a lot of elegant ways to deal with conditional code in Javascript. I didn't even mention inheritance since it works similarly to the object approach I showed above. But if I need multiple instances of something I would of course use polymorphism through inheritance instead.
8 comments:
just to keep things in line for you
var func = kinds[kind];
if (!func)
throw new Error('Invalid kind ' + kind);
func();
->
var func = kinds[kind];
func && func() || throw new Error('Invalid kind ' + kind);
@Rhys, I usually only use the operator syntax when I actually care about the return values.
In the case where I am throwing the Error I don't care about the return value of func().
In any case it won't parse :(
func && func() || throw new Error('Invalid kind ' + kind);
SyntaxError: Unexpected token throw
ah true - throw doesn't work, not even in a ternary. You could always have a global function that errors for you:
err = function(error) {
console.log('error in function' + err.callee);
throw new Error(error);
;};
and use that instead to make it work then do:
func ? func() : err("invalid kind " + kind);
though a better way would probably be to have an assert at the start of your function, then you can just have:
assert(kind[kind]);
kind[kind]();
or if you use closure then you can have an enum with the values, something like:
/**
* @enum {string}
*/
kinds.types = {
BAIRD: 'baird',
MALAYAN: 'malayan'
}
then you can just have a check in the jsdoc that should get picked up by the compiler:
/**
* @param {kinds.types}
*/
var runType = function(type) {
kind[type]();
}
and call it with something like:
runType(kinds.types.BAIRD);
and any errors will get picked up at compile time.
Note that in the callback example, the two functions don't have the same return value if callback is falsy (first will return undefined, second will return callback).
@Rhys, Ah, cool, I didn't know that Closure could pick up on that.
@Golmote, Yes, thats true, you have to beware if it matters to you.
@Rhys: Note that
COND && FUNC() || ERROR();
demands that FUNC() return a true value. So even if COND is true, if FUNC() returns a false value, ERROR() will be called.
COND ? FUNC() : ERROR();
gets around that problem.
---
My comment on the article itself is that it assumes 0 or false is never a valid value to be passed to a function as an argument; it always replaces 0 or false with the default value. I tend to check for null or undefined (based on my needs), or I use the 'arguments' built-in.
@jefferey, you are write about the falsy values, it is worth pointing out. Thanks!
Post a Comment