Working With JavaScript Objects
Article Summary: Explore commonly used object methods for cloning, combining, iterating, destructuring, and property-checking objects. CodePen also provided.
Some of us developers fly by the seat of our pants, carrying out whimsical half-fantasies of morphing jumbled data into gold like Rumpelstiltskin, only to find that we forgot where we placed our thread. “Oh #?!&!, I didn’t realize that my precious object was being mutated by my other object. It’s not fair; I GAVE IT A DIFFERENT NAME FOR GOD’s SAKE!” You don’t want to be one of those guys…
Cloning objects
Oh boy, this one opens up a huge can of worms. Cloning JavaScript objects can be a risky business, and it is for that reason that when you try to find out the best way to do it without some external library, well, you kind of can’t always find instructions on how to do it. To understand these concepts, you must internalize the fact that variables assigned to JavaScript objects are assigned by reference. If you assign a variable to an object, and then assign a new variable to your original variable, the new one is going to point to the same object! Let’s dive in.
Object.assign & the spread operator
These methods allows us to create a shallow clone of an object. Shallow means key: value pairs that don’t have any additional nesting (like more objects or functions) inside of them. Let’s first screw up on purpose and make a boo-boo to prove a point:
// Define your object
const objOne = {
name: 'ATliens',
home: 'GA',
job: 'software equestrianeer',
};// Now create an "idiot-proof" copy, and then mutate it
const objOneCopy = objOne;objOneCopy.name = 'Stupid Mofo';// "Stupid Mofo"!!
console.log('objOne.name = ', objOne.name);
Now that was just silly, wasn’t it? Our new object changed the first one! Who decided that was ever a good idea? Let’s now do it the right way, using assign:
// Make a new object copy with its own memory reference
const objOneCopy = Object.assign({}, objOne);// Now mutate it
objOneCopy.name = 'Nikola Tesla';// "ATliens"
console.log('objOne.name = ', objOne.name);
We could have also done it like this if our platform is ES6 compatible:
const objOneCopy = {...objOne};
Woohoo! We did it! We changed our second object and it didn’t change the original one. Notice the two arguments as well; the first argument represents the “starting” object (we can use an already existing object if we wish to combine them), and the second argument represents the object we are cloning. Now let’s see what happens when we use assign on an object with deep nesting:
// Create an object that contains "deep" nesting (object in object)
const objOne = {
name: 'ATliens',
home: 'GA',
job: 'software equestrianeer',
parents: {
father: 'Luke',
mother: 'Princess Laya',
},
};// Make a copy of it using assign and then mutate the deep reference
const objOneCopy = Object.assign({}, objOne);
objOneCopy.parents.father = 'Nikola Tesla';// "Nikola Tesla', whooooops!
console.log('objOne.parents.father = ', objOne.parents.father);
You see that? We used assign (which worked in the first shallow example), but when we altered the deeply nested father
key on the copy, it changed the original object too! WTF PEOPLE?! It did this because… Well, the good news is that there’s a way around this:
JSON.stringify and JSON.parse
We’ll use the same object as in the example above:
// Make a copy of it using JSON.stringify and JSON.parse
const objOneCopy = JSON.parse(JSON.stringify(objOne));// Now alter the deep object
objOneCopy.parents.father = 'Nikola Tesla';// "Luke" ...it worked!
console.log('objOne.parents.father = ', objOne.parents.father);
That’s all gravvvvy, BUT what if one of our properties is a function:
const objOne = {
name: 'ATliens',
home: 'GA',
job: 'software equestrianeer',
exploreHipHop: function() { return 'Dip-dap'; },
};objOneCopy = JSON.parse(JSON.stringify(objOne));// { name: 'ATliens', home: 'GA', job: 'software equestrianeer' }
console.log(`objOneCopy: ${JSON.stringify(objOneCopy)}`);
What happened? Well, JSON.stringify
will completely omit any object property that is a function (because you can’t really “stringify” a function). There are other things that it won’t stringify as well, but those are out of the scope of this article. Another thing you have to watch out for when using this method is that it will not inherit the object’s prototypal chain.
These two methods will take care of most of your object cloning needs. There are other alternatives, like using Object.create()
(which really is relevant when dealing with constructor functions and prototypal inheritance), and using external libraries like lodash and underscore. If there’s one thing to take away from this, it is this: it’s complicated. At least you now know a couple of common pitfalls and things to watch out for when you next try to clone an object in your project!
Iterating over objects
There will be cases where you might need to loop through the keys or values of an object in order to create an array of values, aggregate some values, or create some new values based on the information in your object. Let’s do this:
Object.keys
This method is the one that you will probably encounter the most. In a lot of cases, it is used in tandem with forEach
to do something with the values. It will return an array of the keys from the target object:
// Define an object containing exotic countries and their capitals
const countries = {
'Mozambique': 'Maputo',
'Timor Leste': 'Dili',
'Djibouti': 'Djibouti',
'Hisbouti': 'Herbouti',
};// ['Mozambique', 'Timor Leste', 'Djibouti', 'Hisbouti']
const countryNames = Object.keys(countries);
console.log(`countryNames: ${countryNames}`);// Loop through the keys and do something with them
let description = '';
const countryDescriptions = Object.keys(countries).forEach((country) => {
description += `${country} is in Africa, `;
});// Mozambique is in Africa, Timor Leste is in Africa, ....
console.log(`description: ${description}`);
Object.values
This is similar to Object.keys
, but it returns an array of the values instead:
const capitalNames = Object.values(countries);// ['Maputo', 'Dili', 'Djibouti', 'Herbouti']
console.log(`capitalNames: ${capitalNames}`);
Object.entries
This one is similar to the keys
and values
methods, but it returns an array of arrays of the key/values pairs:
const capitalsArr = Object.entries(countries);// [ ['Mozambique', 'Maputo'], ['Timor Leste': 'Dili'], ... ]
console.log(`capitalsArr: ${capitalsArr}`);
for .. in
This one is used a lot, but keep in mind that it will only iterate over enumerable properties of the object:
for (let key in countries) {
// key: Mozambique, value: Maputo
console.log(`key: ${key}, value: ${countries[key]}`);
}
Destructuring objects
Destructuring allows us to write nice and concise code. It also makes you look cool. All developers want to be really cool. Let’s assign one object for all of our examples, and then go through a few various techniques and examples!
const destructureObj = {
inventor: 'Nikola Tesla',
origin: 'Croatia',
patent: 'polyphase system',
competitors: {
enemy1: 'Edison',
enemy2: 'DC current',
},
};
First let’s grab one value from the object:
const { inventor } = destructureObj;// Nikola Tesla
console.log(inventor);
Let’s grab a nested item from the object:
const { competitors: { enemy1 } } = destructureObj;// Edison
console.log(enemy1);
Let’s grab and rename a couple of those suckers:
const { origin: hometown } = destructureObj;// Croatia
console.log(hometown);const { competitors: { enemy1: a-hole } } = destructureObj;// Edison
console.log(a-hole);
WHOA though; what if some of those are undefined? Let’s take a look:
const { birthdate } = destructureObj;
const { birthdate: immortal } = destructureObj;// undefined undefined
console.log(birthdate, immortal);const { competitors: { enemy3 } } = destructureObj;// undefined
console.log(enemy3);
Those won’t cause us too much trouble; they’ll just be undefined. We won’t get any errors thrown, however (which could be bad if you needed to know). What happens if we try to deeply destructure a non-existent property though?
const { friends: { none } } = destructureObj;// "Cannot destructure property `none` of 'undefined' or 'null'."
Yep, you guessed it. We will get an error thrown in that case. The good news is that we can prevent this by providing a default value. We can also provide default values for undefined shallow properties as well:
const { friends: { none } = {} } destructureObj;// undefined
console.log(none):const { friends = 'everyone' } destructureObj;// everyone
console.log(friends);
Checking for object properties
There are also times when you will need to check for specific object properties. One example of this might be the following: you are waiting for props to load in React, and you know that when a specific object property is present, that your data has loaded.
‘in’
First, we could use the in
operator:
const anotherObj = {
bestDocumentary: 'Planet Earth',
bestCommentator: 'David Attenborough',
bestEpisode: 'Jungle',
funniestComment: '"wild ass"',
};// It is there!...
if ('bestDocumentary' in anotherObj) {
console.log('It is there!...');
}
This is great, but you need to keep in mind that the in
operator will also check down the prototype chain:
// They are there too!...
if ('toString' in anotherObj && 'hasOwnProperty' in anotherObj) {
console.log('They are there too!...');
}
hasOwnProperty()
We could also use the hasOwnProperty
method. This method is usually better, because it will not check down the prototype chain; it will only check for direct properties:
// Also has bestEpisode...
if (anotherObj.hasOwnProperty('bestEpisode')) {
console.log('Also has bestEpisode...');
}// There is no toString!...
if(!anotherObj.hasOwnProperty('toString')) {
console.log('There is no toString!...');
}
And in summary…
These should provide a well-rounded arsenal of object solutions that you will need in your coding adventures. I hope you liked this article and that it helps you dominate! Here is a convenient codepen if you would like to practice some of these tools: