Get the fundamentals down and the level of everything you do will rise. - Michael Jordan
As stated in my original post, I do 1 hour of video lessons from Watch and Code every day. If you're interested in learning Javascript in a way that goes beyond basic tutorials and gives you a foundational, practical knowledge without relying on frameworks - I'd highly recommend it. If you're reading these posts, please keep in mind that these are just my notes, and I'm not an expert (yet!). If your goal is also to master the fundamentals of Javascript, please head over to Watch and Code and start your journey there!
All screenshots were annotated using Shotty.
defaults
This function extends an object with a defaults object, similar to underscore's _.defaults. It's used for abstracting parameter handling from API methods.
So what the hell does that mean?
Let's take a step back and think about defaults conceptually.
Imagine you have a defaultCar object. Most properties of the car will be pretty standard. You may want to customize something like color, but you probably don't need to customize the number of wheels:
var defaultCar = { wheels: 4, tires: 'standard', color: 'grey' };
So if I wanted to define my own car and tell the dealership what I want, I would probably only specify the color, and the jacobCar object would get its other properties (like number of wheels) by default:
var jacobCar = { color: 'black' };
This is the dynamic we're dealing with when we're talking about defaults. Defaults make it so you don't have to specify certain things.
In the console we can use the defaults function from AccountingJS to give jacobCar the default values we want from defaultCar:
defaults(jacobCar, defaultCar); // Object {color: "black", wheels: 4, tires: "standard"}
Because the defaults function enumerates through the properties in the defaults object (the second argument), and looks to see if the same property exists on the object (the first argument). If that property doesn't exist (is null or undefined) on the object, then it creates a new one of the same name and value as the defaults object. Finally, it returns the object with the new defaults applied. Here's the code:
function defaults(object, defs) { var key; object = object || {}; defs = defs || {}; // Iterate over object non-prototype properties: for (key in defs) { if (defs.hasOwnProperty(key)) { // Replace values with defaults only if undefined (allow empty/zero values): if (object[key] == null) object[key] = defs[key]; } } return object; }
Let's go line by line.
var key;
👆Here we start by declaring a key variable, which we use later on in the function.
object = object || {}; defs = defs || {};
👆Here we have some interesting lines of code. These lines check the corresponding boolean values of the two objects passed in as arguments (object, defs). If the corresponding value is true, it leaves the object alone. But if it's false (undefined, zero, or null), then it will set it to an empty object.
It's unclear why this code is here. It seems it's trying to deal with the case where someone passes in something unexpected and this is a way to recover. Maybe the person calling defaults() fails to pass in anything at all.
Next we have the meat of the function. This is where the action happens:
for (key in defs) { if (defs.hasOwnProperty(key)) { // Replace values with defaults only if undefined (allow empty/zero values): if (object[key] == null) object[key] = defs[key]; } return object; }
👆Here we're going through every property in defs which is the second argument (the default object). In our example case, it would be going through the color, wheels, and tires properties.
Then, in the line:
if (object[key] == null) object[key] = defs[key];
👆We're saying: If object doesn't have this property (key) on it, then use the value from the defs object.
What is hasOwnProperty? Our if statement uses it like this:
if (defs.hasOwnProperty(key)) {
👆This line lets you enumerate through the objects own properties, and not properties than are on the object's prototype. For example, imagine myDog has only one property called name, but the dog prototype object has a method called bark. If we were to enumerate through properties in myDog and console.log each one we would see both name and bark:
for (property in myDog) { console.log(property); } // name // bark
We can modify this easily by adding an if statement:
for (property in myDog) { if (myDog.hasOwnProperty(property)) { console.log(property); } } // name
By putting the console.log inside of the if statement, we guarantee that it only prints myDog's own properties.
Looking back at the defaults function:
Hello, World!
function defaults(object, defs) { var key; object = object || {}; defs = defs || {}; // Iterate over object non-prototype properties: for (key in defs) { if (defs.hasOwnProperty(key)) { // Replace values with defaults only if undefined (allow empty/zero values): if (object[key] == null) object[key] = defs[key]; } } return object; }
Let's look specifically at the line where we apply values from the defs object to the object.
// Replace values with defaults only if undefined (allow empty/zero values): if (object[key] == null) object[key] = defs[key];
👆This isn't the best code. First, it's using ==. Javascript will try to change the types of the two sides of the ==. Better to use === so we have a straight comparison without transforming the left and right sides.
The second problem is that the comment is misleading. It says that the code will "Replace values with defaults only if undefined (allow empty/zero values)". This isn't exactly true.
Let's look at a case where this breaks down. Imagine we haven't decided on our car color, so we set it to null. What we'd want is for the defaults function to extend the first object, but instead it overwrites our color value:
defaults({color: null}, {color: 'grey', wheels: 4}) // Object {color: "grey", wheels: 4}
It ignores the fact that we wanted the color to be null.
What we could do to fix this is:
- Don't write this in one line (this is a style thing).
- Use === instead of ==
- Use undefined instead of null
So this was our old code:
if (object[key] == null) object[key] = defs[key];
And this is our new code:
if (object[key] === undefined) { object[key] = defs[key]; }
Now if we set a property's value to be null it won't be overwritten with defaults, and it has the added benefit of our code actually behaving as the comment suggests.
Summary
- The defaults() function allows you to pass in two objects and apply the properties from the second object to the first one where those properties don't already exist on the first object.
- It does this by enumerating through each property on the second object and checking to see if that property exists on the first object. If the property doesn't exist, it applies that property to the first object.
- This is a way to only need to define "custom" properties when creating a new object, and then setting all the "standard" options to the defaults set in another "default" object.