From time to time, people ask things like “should I use objects to pass function arguments”. I’m sure you recognize the pattern, as it’s common with many libraries:
$.post({ a: 'lot', of: 'properties', live: 'in', here: 'yep' }) |
But I would say this is not actually such a good idea.
Let’s take a look at the best practices for defining function parameter lists in JavaScript.
Clarity is the goal
The problem with passing an object as a parameter to a function is that it’s not clear what values you should put into it.
With libraries like jQuery, it isn’t a big deal – just look at the docs and oh there you go. But in the custom code you’ll run into at work? Just look at the docs… oh wait, what docs? Yeah.
As much as documentation would be nice, custom codebases never have the kind of docs you’d need. If you need to figure out what parameters some function takes, you’re going to have to dig up the source code for it.
This problem is best illustrated by an example. Here’s a function used to throttle some other function:
function throttle(o) { o.threshhold || (o.threshhold = 250); var last, deferTimer; return function () { var context = o.scope || this; var now = +new Date, args = arguments; if (last && now < last + threshhold) { // hold on to it clearTimeout(deferTimer); deferTimer = setTimeout(function () { last = now; o.fn.apply(context, args); }, o.threshhold); } else { last = now; o.fn.apply(context, args); } }; } |
(Original code borrowed from Remy Sharp)
Quick! What values do you put into the parameter object?
You’ll have no freaking clue without reading through the entire function. And even then, you might get it wrong.
Here’s the original signature:
function throttle(fn, threshhold, scope) |
Quick! What parameters does the function take?
If you were defusing a bomb and you had to answer that question or explode, and you only had a few seconds to go… I’m sure the one with separate parameters would be better.
OK, so the first best practice: Prefer separate named parameters for your function
Additionally, separate parameters also help if working in a functional programming style. One common thing in FP is to partially apply your functions, which is much harder to do if your function takes its parameters as an object.
Objects are good for optionals
While separate parameters are a good starting point, we run into trouble as soon as the function has optional parameters.
For functions with a single optional parameter, the solution is simple:
function stuff(requiredA, requiredB, optional)
Best practice: optionals come last
However, if we have more than one optional parameter, the problem is… what if we only need to pass one optional parameter?
function foo(required, optionalA, optionalB) { /* something */ } foo('hello', undefined, 1337); |
Ugh. Having to fill in values like undefined in there is just going to make it more complicated – both to call it, and to handle parameters in the function itself.
In cases with multiple optionals, we can just go back to objects. It doesn’t fully solve the clarity problem, but it means we don’t have to deal with passing extra values.
foo('hello', { optionalB: 1337 });
Best practice: Use an object for optional parameters if there’s more than one
ES6 advantages
Finally, if you’re using ES6 or TypeScript, we can make use of some of the new syntax.
For functions with optional parameters, it isn’t always clear the parameter is optional. With ES6, we can provide a default value to make it more obvious:
function something(a, b = 'default value') { //if b is not passed, its value becomes 'default value' } |
Best practice: Provide defaults for optional parameters
We can also use destructuring in the parameter list with objects, together with a default:
function foo(required, { optionalA, optionalB } = { }) { //available variables: //required, optionalA, optionalB } foo('hi', { optionalA: 1, optionalB: 1337 }); |
This gives you the best of both worlds. You can pass in parameters easily as an object, but the properties for the object can be easily seen from the function’s signature.
Best practice: Make use of ES6 destructuring where it makes sense
Summary
In addition to the suggestions above, using comment blocks before the function declaration is another useful habit to get into. It’s very useful for allowing the reader to get a quick summary not only about the function’s parameters, but also their expected types and any other considerations.
To sum it up:
- Use separate named parameters
- Put optional parameters last
- Use objects for multiple optionals
- Defaults can be used to indicate optional parameters
- Use destructuring to clarify object properties
There’s one more interesting trick you can do using ES6 default parameters. Normally, if you don’t pass a required parameter, nothing happens. But with default params, we can make the code automatically throw an error for missing required parameters! David Walsh has a great summary on that trick here.