JavaScript-mancy: Getting Started Preview - JavaScript Functions Patterns: Defaults

Useful Function Patterns: Default Arguments

Put yourself in the place of the listener,
of the eater,
of the reader,
of the user of your carefully crafted spells,

Think from the outside in,
and you'll be rewarded manyfold.

Think developer experience!!

- Llroc,
Warrior Poet
/* 
 
A distant sound....
 
A booming sound...
  
Or a... the flapping of the wings of some giant bird
 
Salt in your mouth...
  
clap! clap! clap!

*/

randalf.says('Well that was close... but I am impressed!');

moolean.wakesUp(['slowly', 'groggily']);
mooleen.laughsWeakly();

/*

Mooleen awakes to an infinite sea, a vast ocean, and dunes
as far as the eye can see.

*/

mooleen.says('I am a fast learner...')
mooleen.says('And now I want to learn what' + 
    ' the heck is going on?');

randalf.says('There will be time for that soon. ' + 
    'Now you need to recover.');

mooleen.says('but...');

randalf.says('You know... I think you can add some improvements ' + 
'to those spells... I know a couple of tricks... ' + 
'have you heard of defaults?');

Have You Heard Of Defaults?

I don’t know you but I’m always trying to write less code and build beautiful APIs for myself and other developers. One small way to achieve that is by using default arguments. Defaults let you add more intention behind your APIs and provide a shortcut to the most common functionality or task carried out by a function.

In this and the next few chapters we’ll discuss several patterns that you can use to improve the usability of the functions you write in JavaScript: defaults, multiple arguments and function overloading. Let’s get started with defaults in ES5 and ES6.

Using Default Arguments in JavaScript Today

If you have had the chance to take a look at some Javascript code, you may have encountered the following statement and wondered what (the heck) it meant:

var type = type || "GET";

Hold on tight because we are about to unveil the mystery. That statement right there has been the prevalent approach to writing defaults in JavaScript for ages. If you do a little of a mental exercise and try to remember what you read about the || (OR) operator in the introduction you’ll recall that this operator behaves in quite a special way: It returns the first truthy expression of those being evaluated or the last expression if none is truthy. As usual, this behavior is better illustrated through an example:

> false || 42 || false
// => 42

> "" || false
// => false

> "" || {}
// => Object {}

> var numberOfPotatos = undefined
> numberOfPotatos || 3
// => 3

> numberOfPotatos = 5
> numberOfPotatos || 3
// => 5

Since up until ES6 there was no native support for defaults and using the || operator was the most compact way to achieve it, this pattern soon became the de facto standard for defaults throughout the community and is often used in JavaScript applications. You just need to take a sneak peak in any popular open source library to find it used in innumerable situations:

// from jquery loads.js 
// (https://github.com/jquery/jquery/blob/master/src/ajax/load.js)
type: type || "GET",

// or from yeoman router.js 
// (https://github.com/yeoman/yo/blob/master/lib/router.js)
this.conf = conf || new Configstore(pkg.name, {
    generatorRunCount: {}
  });

One of the most common use of defaults happens when evaluating the arguments of a function like in this castIceCone spell:

function castIceCone(mana, options){
  // we take advantage of the || operator to define defaults
  var mana = mana || 5,
    options = options || {direction: 'forward', damageX: 10},
    direction = options.direction || 'forward',
    damageX = options.damage || 10,
    damage = 2*mana + damageX;
    
  console.log("You spend " + mana + 
              " mana and cast a frozen ice cone " + direction + 
              " (" + damage + " DMG).");
}

The castIceCone function has two arguments: a mana argument that represents the amount of mana a powerful wizard is going to spend in casting the ice cone and an additional options object with finer details.

The function makes extensive use of the || operator to provide defaults for all possible cases. In the simplest and most convenient of scenarios the user of this function would just call it directly, and when more finesse is needed she or he could populate the richer options argument:

castIceCone();
// => You spend 5 mana and cast a frozen ice cone forward (20 DMG)
castIceCone(10);
// => You spend 5 mana and cast a frozen ice cone forward (20 DMG)
castIceCone(10, { damage: 200});
// => You spend 5 mana and cast a frozen ice cone forward (220 DMG)
castIceCone(10, { direction: 'to Mordor'})
// => You spend 10 mana and cast a frozen ice cone to Mordor (30 DMG)
castIceCone(10, { direction: 'to Mordor', damage: 200})
// => You spend 10 mana and cast a frozen ice cone to Mordor (220 DMG)

An alternative way to do defaults is to wrap them within an object.

Defaults with Objects

In addition to relying on the || operator, you can use an object to gather your default values. Whenever the function is called you merge the arguments provided by the user with the object that represents the defaults. The default values will only be applied when the actual arguments are missing.

You can define a mergeDefaults function to perform the merge operation:

function mergeDefaults(args, defaults){
  if (!args) args = {};
  for (var prop in defaults) {
    if (defaults.hasOwnProperty(prop) && !args[prop]) {
      args[prop] = defaults[prop];
    }
  }
  return args;
}

And then apply it to the arguments passed to the function like in this castLightningBolt spell:

function castLightningBolt(details){
  // we define the defaults as an object
  var defaults = {
  	mana: 5,
    direction: 'forward'
  };
  // merge details and defaults
  // properties are overwritten from right to left
  details = mergeDefaults(details, defaults);
  
  console.log('You spend ' + details.mana + 
      ' and cast a powerful lightning bolt ' + 
      details.direction + '!!!');
}

Which provides a similar defaults developer experience to that of the first example:

castLightningBolt();
// => You spend 5 and cast a powerful lightning bolt forward!!!
castLightningBolt({mana: 100});
// => You spend 100 and cast a powerful lightning bolt forward!!!
castLightningBolt({direction: 'to the right'});
// => You spend 5 and cast a powerful lightning bolt to the right!!!
castLightningBolt({mana: 10, direction: 'to Mordor'});
// => You spend 10 and cast a powerful lightning bolt to Mordor!!!

More often though you will probably rely on a popular open source library. Libraries like jQuery, underscore or lodash usually come with a lot of utility functions that you can use for this and many other purposes. For instance, jQuery comes with the $.extend function and underscore comes with both the _.defaults and _.extend functions that could help you in this scenario.

Let’s update the previous example with code from these two libraries:

function castLightningBoltOSS(details){
  // we define the defaults as an object
  var defaults = {
  	  mana: 5,
      direction: 'in front of you'
  };
  // extend details with defaults
  // properties are overwritten from right to left
  // jQuery:
  details = $.extend({}, defaults, details);
  // underscore:
  //details = _.extend({}, defaults, details);
  
  // to use defaults switch argument places
  // properties are only overwritten if they are undefined
  // underscore:
  //details = _.defaults({},details, defaults);
  
  console.log('You spend ' + details.mana + 
      ' and cast a powerful lightning bolt ' + 
      details.direction + '!!!');
}

If you have kept an eye on ES6 you may know that it comes with a native version of the jQuery $.extend method called Object.assign. Indeed, you can update the previous example as follows and achieve the same result:

//details = $.extend({}, defaults, details);
details = Object.assign({}, defaults, details);

However, if you are planning to use ES6, there’s an even better way to use defaults.

Native Default Arguments with ECMAScript 6

ES6 makes it dead easy to declare default arguments. Just like in C# you use the equal sign "=" and assign a default value beside the argument itself:

function castIceCone(mana=5){
  console.log(`You spend ${mana} mana and casts a terrible ice cone`);
}

castIceCone();
// => You spend 5 mana and casts a terrible ice cone
castIceCone(10);
// => You spend 10 mana and casts a terrible ice cone

JavaScript takes defaults even further because they are not limited to constant expressions like C# optional arguments. In JavaScript, any expression is a valid default argument.

For instance, you can use entire objects as defaults:

function castIceCone(mana=5, options={direction:'forward'}){
  console.log(`You spend ${mana} mana and casts a ` + 
    `terrible ice cone ${options.direction}`);
}

castIceCone();
// => You spend 5 mana and casts a terrible ice cone forward
castIceCone(10);    
// => You spend 10 mana and casts a terrible ice cone forward
castIceCone(10, {direction: 'to Mordor'});
// => You spend 10 mana and casts a terrible ice cone to Mordor
castIceCone(10, {duck: 'cuack'});
// => You spend 10 mana and casts a terrible ice cone undefined

If you take a closer look at the end of the example above, you’ll realize that we have a small bug in our function. We are setting a default for the entire options object but not for parts of it. So if the developer provides an object with the direction property missing, we will get a strange result (writing undefined to the console).

We can solve this problem by taking advantage of the new destructuring syntax which allows you to assign argument properties directly to variables within a function, and at the same time provide defaults to parts of an object:

function castIceConeWithDestructuring(mana=5, 
          {direction='forward'}={direction:'forward'}){

  console.log(`You spend ${mana} mana and casts a ` + 
    `terrible ice cone ${direction}`);
}
castIceConeWithDestructuring();
// => You spend 5 mana and casts a terrible ice cone forward
castIceCone(10, {direction: 'to Mordor'});
// => You spend 10 mana and casts a terrible ice cone to Mordor
castIceConeWithDestructuring(10, {duck: 'cuack'});
// => You spend 10 mana and casts a terrible ice cone forward

In this example we use argument destructuring {direction='forward'} to:

  • Extract the property direction from the argument provided to the function at once. This allows us to write direction instead of the more verbose options.direction that we used in previous examples.
  • Provide a default value for the direction property in the case that the function is called with a options object that misses that property. It therefore solves the problem with the {duck: 'cuack'} example.

Finally, taking the freedom of defaults to the extreme, you are not limited to arbitrary objects either, you can even use a function expression as a default (I expect your mind has just been blown by this, this… very… second):

function castSpell(spell=function(){console.log('holy shit a callback!');}){
  spell();
}

castSpell();
// => holy s* a callback!
castSpell(function(){
  console.log("balefire!!!! You've been wiped out of existence");
});
// => balefire!!!! You've been wiped out of existence

Concluding

Yey! In this chapter you’ve learned several ways in which you can use defaults in JavaScript whether you are using ES5 or ES6 and beyond. Taking advantage of defaults will let you write less code and provide a slightly better user experience to the consumers of the functions that you write.

Up next, more function patterns with multiple arguments and the rest operator!

randalf.says('See? By using defaults you can make ' + 
             'your spellcasting more effective');

mooleen.says('hmm... indeed... indeed...');
mooleen.says('Do I get to know why I am here? And what here is?');

randalf.says('Sure! Just start practicing while I go get us lunch');

Exercises

function fire(mana, target){
  if (mana > 10) 
    console.log('An enormous fire springs to life on ' + target);
  else if (mana > 4) 
    console.log('You light a strong fire on ' + target);
  else if (mana > 2) 
    console.log('You light a small fire on' + target);
  else if (mana > 0) 
    console.log('You try to light a fire but ' +
    'only achieve in creating teeny tiny sparks. ' + 
    'Beautiful but useless.');
}

Solution

mooleen.says('damn old man...');
mooleen.says('let me modify this spell for the most' + 
             ' cost-effective use case');

function fireImproved(mana, target){
  mana = mana || 3;
  target = target || 'dry wood';
  if (mana > 10) 
    console.log('An enormous fire springs to life on ' + target);
  else if (mana > 4) 
    console.log('You light a strong fire on ' + target);
  else if (mana > 2) 
    console.log('You light a small fire on ' + target);
  else if (mana > 0) 
    console.log('You try to light a fire but ' + 
                'only achieve in creating teeny tiny sparks.' + 
                ' Beautiful but useless.');
}

mooleen.weaves('fireImproved()');
// => You light a small fire on dry wood;
mooleen.says('aha!');

Solution

function fireImprovedES6(mana=3, target='dry wood'){
  if (mana > 10) 
    console.log('An enormous fire springs to life on ' + target);
  else if (mana > 4) 
    console.log('You light a strong fire on ' + target);
  else if (mana > 2) 
    console.log('You light a small fire on ' + target);
  else if (mana > 0) 
    console.log('You try to light a fire but ' + 
      'only achieve in creating teeny tiny sparks.' + 
      'Beautiful but useless.');
}

mooleen.weaves('fireImprovedES6()');
// => You light a small fire on dry wood

randalf.says('Excellent! Nothing like a bonfire for telling a good story!');