By adopting destructured objects with default parameters in place of regular parameters, we can make our reusable functions highly versatile and resilient. This allows them to grow in functionality over time, without refactoring call sites.
How
The basic idea is that instead of writing a function like this
function myFunction (myString, myNumber) { /* ... */ }
we write it like this
function myFunction ({ myString = '', myNumber = 0 } = {}) { /* ... */ }
What is all this curly-braces-and-equals-signs nonsense, you ask? It might not look very intuitive, but all we're doing is grouping the parameters in a single object and giving them default values.
Let's break it down. ES6 provides two amazing language features — default parameters and object destructuring.
The first is a way to provide defaults to regular function parameters. The syntax is rather simple:
function myFunction (myString = '', myNumber = 0) { /* ... */ }
Just add an equals sign and a value. The value could also be a boolean, an object, an array, or even null
. This is very useful for making parameters optional, so that rather then defaulting to undefined
they have a value to fall back on.
"Defaults! Great! Reming we why we're bothering with all the destructuring business?"
Well, if you're absolutely positive that you'll never, ever, ever, ever, ever change anything about what any of your functions do, ever, you can stop reading here.
Still here? I had a feeling.
The short answer is that function parameters are indexed by their order, whereas objects are indexed by key. Here's the longer answer, after which we'll look at a real life example.
Why
Javascript is a very (very!) loosely typed language. The kind of loose where {} + [] === 0
returns true
. Seriously. This forgiving nature of the language is by design, and in many cases a great benefit. When we write client-side Javascript, we know almost nothing about the environment in which our code is going to run — what operating system, which browser, what browser version, even which Javascript runtime will be running it or what it supports. It's all quite dynamic, in ways that programmers from other fields might call crazy. But it's our web, and we love it.
Forgiving can easily turn into confusing, though. Worse, loose typing makes room for the kinds of bugs that, once fixed, leave us feeling utterly silly. Like confusing the order of arguments in a function call. Switch a DOM element parameter with some boolean flag, and suddenly you're hit with a not-as-helpful-as-it-seems-to-think TypeError
, telling us that, apparently, the browser Cannot read property 'foo' of undefined
. Since Javascript treats the arguments passed to a function as an array, their names have no meaning outside of it.
This can become a real problem when we want to add functionality to an existing function. The only way to be sure it won't break all existing calls, is to add the new parameter at the end, and make sure the function can handle the new parameter possibly being undefined
.
This can get problematic very quickly.
Real Life Example™
Say we have a (simplified) function that sets up a slideshow for each slideshow container in the page:
function setupSlideshow (interval) {
document
.querySelectorAll('[data-slideshow-container]')
.forEach($container => {
const $slides = $container.querySelectorAll('[data-slide]')
let currentIndex = 0
function setIndex () {
$slides.forEach(($slide, slideIndex) => {
$slide.toggleClass('active', slideIndex === currentIndex)
})
}
// ... all kinds of code to track state, looping, etc
const timer = setInterval(() => {
setIndex(currentIndex + 1)
}, interval)
})
}
setupSlideshow(3000)
So far so good. We get the slideshow container and the slides, we track an index, we toggle a class, and we change slides every n
milliseconds, based on the single parameter.
And then life happens
We successfully use this function for several sites, only to find ourselves in a bit of a pickle — in our current project, we can't use active
as the toggled classname. Oh, and we need to add customizable classes to the previous and next slides, as well. Oh, and wait, the container can't have data-
attributes on it, so it has to be selected by class name.
Technically this isn't much of a problem, and adding the required parameters (with defaults!) seems simple enough:
function setupSlideshow (
interval = 3000,
containerSelector = '[data-slideshow-container]',
toggledClass = 'active',
prevClass = 'prev',
nextClass = 'next'
) {
/* ... */
}
setupSlideshow(3000, '.slideshow-container', 'current-slide')
Amazing work!
On our next project we find that the defaults we set are great. But for whatever reason, we need to customize just the previous slide class name. This means w'll need to explicitly pass all the earlier arguments as well:
setupSlideshow(
3000,
'[data-slideshow-container]',
'active',
'special-classname-for-a-super-special-slide'
)
"My defaults! [sobbing] My beautiful defaults!"
I know how you feel. Not to mention, looking at this function call, it might not be clear what each parameter is. "Everybody knows that the 3rd parameter of a slideshow setup function is the toggled class name" said no one, ever, hopefully.
Destructuring to the rescue!
What if our function was constructed like this:
function setupSlideshow (args) {
// ...
const $slides = $container.querySelectorAll(args.toggledClass)
// ...
}
const myArgs = { toggledClass: 'active', /* ... othes */ }
setupSlideshow(myArgs)
Explicit argument naming! No more indexing by order! But, alas, where have all the defaults gone?
We could use a default parameter for args
:
function setupSlideshow (
args = { toggledClass: 'active', /* ... others */ }
) {
/* ... */
}
But object destructuring inside the function's parameters allows us a more elegant way of writing essentially the same thing:
function setupSlideshow ({ toggledClass: 'active', /* others */ } = {}) {
/* ... */
}
Basically we're passing in a default args
object, and using an additional default (that's the weird-looking = {}
at the end) to make sure the object is never undefined
. Kind of like a default for the default, just that it applies to each value on the object. This means that whatever key-value pairs we don't pass in our call will just default.
So we can write our function as
function setupSlideshow ({
interval: 3000,
containerSelector: '[data-slideshow-container]',
toggledClass: 'active',
prevClass: 'prev',
nextClass: 'next'
} = {}) {
/* ... */
}
setupSlideshow({
prevClass: 'special-classname-for-a-super-special-slide'
})
and all of the unspecified values will be set to their defaults when the function runs.
The next time we need to add more customization, we can just add it to the object, anywhere we want. This can be very important when reusing the same code in many projects — we can add new customization options without breaking old function calls. Say we need an autoplay
parameter:
function setupSlideshow ({
interval: 3000,
containerSelector: '[data-slideshow-container]',
toggledClass: 'active',
prevClass: 'prev',
nextClass: 'next',
autoplay: false
} = {}) {
/* ... */
}
setupSlideshow({
autoplay: true
})
Assuming the function's internal logic does nothing different if autoplay
is false, all of the existing calls will keep working with no change.
Another advantage of this pattern is that we can easily separate required arguments from optional ones — anything without a default can be a regular parameter, and everything optional can be in our single object:
function setupSlideshow (slideshowUniqueId, {
interval: 3000,
containerSelector: '[data-slideshow-container]',
toggledClass: 'active',
prevClass: 'prev',
nextClass: 'next',
autoplay: false
} = {}) {
/* ... */
}
setupSlideshow('slideshow-5', {
interval: 7000
})
Conclusion
On the web, as in life, change is (sometimes) the only constant. Writing code that can change gracefully is an important skill, and it's worth it to always be asking yourself "what if this bit needs to change at some point?".
It's a bittersweet irony that dynamic typing sometimes leads to rigidity. Flexible coding can help.
Top comments (2)
This is not functional, though. A function in the sense of the functional paradigm behaves like a pure mathematical one. I wouldn't bother you with this comment, but you tagged your post #functional...
Whoops, that was by mistake. Thanks.
Some comments have been hidden by the post's author - find out more