My first experience with default arguments was from Python, where there are dire warnings of the dragons you will encounter if you supply mutable values as default arguments. The concern is in Python, these arguments are initialized at the time that the function is created, which can give you some very surprising results.
def greeter(greetings={}):
print(greetings.get('hi', 'No such greeting')
greetings['hi'] = 'Hello World!'
When you first run this function with no arguments, 'No such greeting' will be printed on the screen, because it will be looking in the empty dictionary for the key 'hi'. However, every time you run the function after that, 'Hello World!' will be printed on the screen. This happens because we modified the default argument, which is the same dictionary every time you run the function.
Because of this experience, I had been very wary of default arguments in javascript, where mutable objects are common and it seemed rife with danger. However, when I started noticing that some of my peers used this pattern fairly regularly, I went looking to try and replicate the problems that we see in Python.
const greeter = (greetings={}) => {
console.log(greetings.hi || 'No such greeting')
greetings.hi = 'Hello World!'
}
Much to my surprise, this function very reliably outputs 'No such greeting'. This was very surprising to me. If the arguments of the function weren't initialized when the function was called, when were they initialized?
const shouter = (arg=console.log('Hello World!')) => null
When you write this function, no log appears, so the arguments definitely aren't initialized when the function is written. Instead, each time you run the function, the log appears, suggesting that the arguments are reinitialized with each invocation of the function. This solves the complications we saw in python, where the values were shared across function calls, but also introduces another question. If the arguments are initialized when the function is called, what is in scope for them? Can they depend on each other?
const interdependent = (one, two=one+1) => two
// interdependent(1) === 2
// interdependent(2) === 3
It turns out that javascript default arguments are in the same namespace as the body of the function, with the added restriction that they can only refer to arguments which came before them in the argument list. For instance, (one=two-1, two) => one
will return a ReferenceError whenever the first argument is undefined, because it tries to reference the variable two
before it is defined. Another way to understand this is to look at how defaulting arguments in javascript used to work.
function interdependent(one, two) {
var two_within_the_function
if (typeof two === 'undefined') {
two_within_the_function = one + 1
} else {
two_within_the_function = two
}
return two_within_the_function
}
Imagine that each default argument is in such a block, where we are within the function body and checking if the variable is undefined. In this case it is clearer why we couldn't refer to the arguments out of order. the within_the_function
versions of the later variables simply would not have been defined yet.