Depth of JavaScript Objects

I dig into the JavaScript Objects and discuss Objects to a substantial extent based on my personal opinions and experiences

Aravinda Rathnayake
17 min readMar 14, 2021
A PUPPY WAITING FOR HEAR A LONG STORY… (Photo by Marcus Wallis on Unsplash)

Prerequisites

You should have a basic understanding and experience in JavaScript, plus if you have basic knowledge of objects, that was perfect. Otherwise, you may have confusion or difficulties, especially if you are coming from an object-oriented programming paradigm.

Let’s start from the scratch

If you are interested in science, you heard about everything in this universe is a different type of object. It may have billions of copies (includes M87 black hole kind of black hole object too 😉) to represent each type of object.

EXAMPLE — Think a puppy is an object, and he can have tons of copies with different characteristics (breed, name, color, vaccinations) and behaviors (bark, run, wiggle). Still, we all know it is a kind of puppy.

Alright!, that’s all about real-world objects. Now you might come up with a question about how it can be applicable for JavaScript objects.

Data Types of JavaScript

JavaScript is a language that interprets and considers almost everything as non-primitive types (object/ reference type), and the rest of the things are considered primitive types (literal type).

Then, what are the primitive types? :

  • [string, number, boolean, null, undefined, symbols and bigint]

Okay, Fine, now what are the rest of the things? :

  • [String, Number, Boolean, Object, Function, Array, Date, RegExp, Error]

NOTE — If you try to figure out the typeof null you might get it to type as Object, so you may get confused.. what the heck is it? Is it supposed to be a type of null? Definitely, you’re right!. This misconception is introduced by the language itself, in fact, null is a type of another primitive.

TIP 01— First letter in all the literals we usually define in lower case characters. But when constructing objects, it begins with an uppercase first letter with a new keyword.

TIP 02 — Do practice not to construct your data types as object or object subtypes (Function, Array, etc.). Unless you have a considerable deal with instances.

I DON’T explain these literal types (primitives) because I have focused on Objects in this article, but if you feel that you have missed something in literals, you can refer MDN JavaScript Data Types section.

Alright, let’s continue. As we said above, except for these primitives, anything in Java Script is considered an Object. Pretty cool, huh? Let’s dig deeper and see what happens inside.

Primitive vs. Non-primitive Memory Allocation

You see an exact difference, right? in the JavaScript engine, all the primitives are stored in the stack as an absolute value. When it comes to the objects, these store a reference on the heap that points to a specific object. Now you should understand why we called objects are reference types and primitives are value types.

Creating Objects

Then what is an Object? What were the characteristics of an Object? Why do we use it for? Alright!, an object can be identified as a container for several properties (characteristics and behaviors) with key-value pairs where the key is always a string (identifier), and value can be anything.

Keep in mind, you can have anything as your property name that can be transformable to string. You can have spaces between letters, numbers, booleans. Even you can have empty double quotes as a property name, and the javascript engine will automatically transform your key into a string in run time. But accessibility may differ (will explain in Accessing & Defining Properties section), relatively simple right!.

Objects can be construct by using four distinct methods, which are:

  • Object Initializers
  • Using Function constructor
  • Using ES6 classes
  • Using Object.create() method

EXAMPLE — Puppy have a name called “Rashi,” she is a breed of “German Shepard” and currently the age at 12 years. Now let’s see how we can define Puppy Object using all the object-defining methods.

// Using Object initializer
const puppy = {
name: 'Rashi',
breed: 'German Shepard',
age: 12
}

// Using Function constructor
function PuppyFunction() {
this.name = 'Rashi';
this.breed = 'German Shepard';
this.age = 12;
}
// Using ES6 classes
class PuppyClass {
constructor() {
this.name = 'Rashi';
this.breed = 'German Shepard';
this.age = 12;
}
}

const puppyTwo = new PuppyFunction();
const puppyThree = new PuppyClass();
// Use console.log() to print data
puppy.name; // "Rashi"
puppyTwo.breed; // "German Shepard"
puppyThree.age; // 12

Wait for a second, you missed one way of a define? Keep focus!. The last constructing way is Object.create() before that, you must have a good idea about Prototypes and Prototype chain.

Prototype and Prototypal Chain

From the beginning, I have told you almost everything in JavaScript is an object. Okay, fine, is everything? NO! the exciting thing is that the object can have a prototype to extend its properties, and when finding a particular property on an object, these levels are consulted if the object has no property by itself.

If the object prototype did not found the expected property, it would also consult from the parent of the Object’s prototype. If still not found, it will again consult from a grandparent and keep going until it meets the Object.prototype where the top level of object hierarchy will have no parent, which means the parent is null and returns null if the property was not found on all the levels.

The process of traversal/ lookup is called the Prototype chain. This is kind of like an OOP but not precisely behaves the same. Let’s take a look at an example.

function Animal(name) {
this.name = name;
}

Animal.prototype.sleep = function() {
console.log(`${this.name} Zzz...`);
}

function Puppy(name) {
this.name = name;
}

Puppy.prototype = Object.create(new Animal());

Puppy.prototype.makeSound = function() {
console.log(`${this.name} Woof! Woof!`);
}

const puppyInstance = new Puppy('Rashi');
// Use console.log() to print data
puppyInstance.makeSound(); // "Rashi Woof! Woof!"
puppyInstance.sleep(); // "Rashi Zzz..."

TIP 03 — If you are concerned about what${} actually does, its mechanism of interpreting embedded expressions inside a string literals (similar to string interpolation) called Template Literals.

BASE SCENARIO — We have passed the “Rashi” string to the Puppy function constructor and hold the reference using the puppy variable. Then we have called a set of methods and a property using the preserved reference.

SCENARIO 01— In this case, the JS engine will execute the makeSound() method and find makeSound() the function constructor’s own property. If it is not, then it will check makeSound() is on the object prototype. If it is found on the object prototype, then execute the method with the respective data.

SCENARIO 02 — Same as the first scenario, JS engine keeps looking; the particular method is a property of an object. If not, then traverse to parent prototype (Animal) and check whether sleep() is available in the context. If true, then execute the method with respective data.

SCENARIO 03 — This case is the worse case that can happen. You are searching for a property on a puppy object. First, if it is not available as the same as the previous, it will keep traverse until Object.prototype becomes null, and on that point, the JS engine will return null as the value.

Let’s take a look graphical representation of our scenarios.

Prototype Sample Usage (Graphical Representation)

NOTE — Keep this in remember, each time creating an object, the JS engine will automatically trigger a new Object() implicitly, whether it is a Function, Array, or whatever type of Object. Bit of tricky, right? Let’s take a look sample.

const ageNumber = 12; // 12
const ageString = ageNumber.toString(); // "12"

// Use console.log() to print data
typeof ageNumber; // "number"
typeof ageString // "string"
// Use console.log() to print data
ageString.__proto__ === String.prototype; // true
ageString.__proto__.__proto__ === Object.prototype; // true
// Use console.log() to print data
ageString
.__proto__.__proto__.__proto__ // null

Here, what we did is we have created a variable to store a number and assign the string converted number into another variable.

In this question, first, we have access object own prototype, access its parent prototype, and strictly compare whether its value and type are precisely the same.

Little question, where do these toString() method come from? Did we define it? Is it a part of literals? Hell no! that was a method of String.prototype. We are reusing parent properties and methods. that’s why __proto__is exactly String.prototype and __proto.__proto__ is became Object.prototype.

For the demonstration purpose, we have access __proto__ property directly through the browser because browsers implement prototypes through __proto__ property. But JavaScript strictly never recommends you directly access or re-assign a value for it because it may break your whole prototype chain level.

Okay, now you know the prototypes, and it’s hierarchy.. let’s continue from where we stopped.

Object.create()

This method creates a new object, using an existing object as the newly created object prototype. for more details refer Object.create()section in MDN.

const puppy = {
message: function () {
console.log(`${this.name} is a ${this.breed}`);
}
}

const animal = Object.create(puppy);

animal.name = 'Rashi';
animal.breed = 'German Shepard';

// Use console.log() to print data
animal.message();

Accessing & Defining Properties

There are two standard methods for accessing and defining properties once an object was created. You can use dot notation and bracket notation. Basically, these two operators are doing the same operation, but the behaviors are pretty different.

Basically, dot operator requires, Identifier compatible property names and bracket notation can accept any UTF-8/ unicode compatible string or computed property names (introduced in ES6) as the name of the property.

const puppy = {};

// Dot notation basic usage
puppy.name = 'Rashi';
console.log(puppy.name); // "Rashi"

// Bracket notation basic usage
puppy[true] = 'A true puppy';
console.log(puppy['true']); // "A true puppy"

puppy[4] = 'Have 4 paws';
console.log(puppy['4']); // "Have 4 paws"

puppy[puppy] = 'Smart puppy';
console.log(puppy['object Object']) // Smart puppy

// Computed property name with bracket notation
const identifier = 'age';
puppy[identifier.toUpperCase()] = 12;
console.log(puppy['AGE']); // 12

JS supports a native method for property definition called Object.defineProperty() & Object.defineProperties()which was do the property or multiple properties definition with supported configurations.

However, so far from my experience, I recommend you provide meaningful and uniquely identifiable property names in a standard format while maintaining the code consistency (camel case, pascal case, etc.) because it is more readable the developer next to you and you on later as well.

Objects Comparison

When it comes to comparing objects, you should always keep in mind what we described in the introduction sections.

const puppy = {
name: 'Rashi',
breed: 'German Shepard',
isVaccinated: true,
age: 12
};

const clonePuppy = {
name: 'Rashi',
breed: 'German Shepard',
isVaccinated: true,
age: 12
};

// Use console.log() to print data
puppy == clonePuppy; // false
puppy === clonePuppy // false

Here what we did is simply creates two different objects with the same key-value pairs and compare them by:

  • loosely compare (double equal) → compares the value only
  • strictly compare (triple equal) → compares the value and data type

Okay, now what was wrong in here? Why comparison returns falsely? Because two distinct objects are never equal, even their values and property names are the same because they point to two different memory references. Let’s move into another scenario.

const puppy = {
name: 'Rashi',
breed: 'German Shepard',
vaccinations: {
type: 1,
vaccine: 'parvo'
}
};

const clonePuppy = puppy;

// Use console.log() to print data
clonePuppy.name === puppy.name; // true
clonePuppy.vaccinations === puppy.vaccinations; // true

Here, what we do is simply create an object literal and assign its reference to another variable. In other words, clonePuppy is pointing to the same object in memory which means even we have compared its value or type them, it returns true.

But there was a problem in this way. Let’s discuss and dig more details about cloning objects in the next section called Object Copying.

Object Enumeration (Iteration)

Think we want to iterate an object recursively and transform its properties. In this case, enumeration (iteration) has come to the action. This was behaving the same as for loop, but keeping in mind for loops can be used to iterate such as Arrays, but we need to iterate object properties in our case.

You should have a fair idea about the following attributes (Just grab them from MDN as is 😉).

  • configurable -- true if and only if the type of this property descriptor may be changed and if the property may be deleted from the corresponding object (Default → false).
  • enumerable -- true if and only if this property shows up during enumeration of the properties on the corresponding object (Default → false).
  • writable -- true if and only if the value associated with the property may be changed (data descriptors only) (Default → false).
  • value -- The value associated with the property (data descriptors only). (Default → undefined).

01. for…in Loop

for…in loop can be used to iterate an object with its all the enumerable (shows only if enumerable is true in property configuration) and the non-symbol properties in it.

However, in this case, the prototypal chain is served. This means objects, not own user-defined properties may be shown if we did not explicitly filter our object.

const puppy = {
name: 'Rashi',
breed: 'German Shepard',
isVaccinated: true,
age: 12
};

// Prints all properties in hierarchy
for (const identifier in puppy) {
console.log(puppy[identifier]);
}

TIP 04 in operator is an existence checker, it returns true if a specific property is available inside the object or object prototypal chain.

So, how can we mitigate that issue and concern about only the object’s own properties? Just simply follow the below example.

const puppy = {
name: 'Rashi',
breed: 'German Shepard',
isVaccinated: true,
age: 12
};

// Prints all object own properties
for (const identifier in puppy) {
if (puppy.hasOwnProperty(identifier)) {
console.log(puppy[identifier]);
}
}

Now seems alright!. Our loop now returns only object-owned properties. But where does it comes hasOwnProperty()? We have described wherefrom those literal properties are come from. but what it does? hasOwnProperty() methods returns a boolean whether an object has the specific property as its own property. for more details refer the Object.hasOwnProperty() in MDN.

But this mechanism of displaying the object’s own properties is too lengthy. Is there a more simple method to overcome both cases? YES! for that, JS has given us few intelligent native ways.

02. Object. keys()

This method will return an array of a given object’s all enumerable own property names in the same order of contains.

const puppy = {
name: 'Rashi',
breed: 'German Shepard',
isVaccinated: true,
age: 12
};

// Use console.log() to print data
Object.keys(puppy); // ["name", "breed", "isVaccinated", "age"]

For more details refer Object.keys() section in MDN. Alright!. but how about if we need directly iterate object property values? you can use Object.values() for that.

03. Object.values()

Same as the Object.keys() the only difference is it returns an array for given Object all enumerable own property field values in the same order of it contains. Similar to for…in loop, but this will return the Object’s own property values only. For more details refer Object.values() section in MDN.

const puppy = {
name: 'Rashi',
breed: 'German Shepard',
isVaccinated: true,
age: 12
};

// Use console.log() to print data
Object.values(puppy); // ["Rashi", "German Shepard", true, 12]

Alright! Now we know how to access three methods of Object. How about if you need to access the whole entries? for that Object.entries()comes to the action.

04. Object.entries()

It returns an array of arrays object’s own enumerable both key value pairs in the same order it contains. For more details refer Object.entries() section in MDN.

const puppy = {
name: 'Rashi',
breed: 'German Shepard',
isVaccinated: true,
age: 12
};

// Use console.log() to print data
Object.entries(puppy); // [["name", "Rashi"], ["breed, "German Shepard"], ["isVaccinated, true"], ["age", 12]]

Immutable Objects

Sometimes you may want to make properties that cannot be changed either by accident or intentionally, for that JS supports a set of methods to do that.

But keep in note these all approaches create shallow immutability. This means if an object is pointing to the same reference of another object, the object’s content is not affected and remains its mutability.

01. Object Constant

Immutability can be achieved by using Object.defineProperty()and Object.defineProperties(), by simply combining writable: false and configurable: false will creates a property which can’t be modify, redefine or delete.

const puppy = {};

Object.defineProperty(puppy, 'name', {
value: 'Rashi',
configurable: false,
writable: false
})

puppy.name = 'Rex';

// Use console.log() to print data
puppy.name; // "Rashi";

02. Object.seal()

This method can be used to seal the object and prevent new properties from being added and marking all the properties configurable: false, which means you can’t re-configure or delete properties in the object anymore, but values can still be changed long as the properties are writable.

TIP 05 — Use Object.getOwnPropertyDescriptors() for check object properties configurable status.

const puppy = {
name: 'Rashi'
};

Object.seal(puppy);

// // Use console.log() to print data
Object.getOwnPropertyDescriptors(puppy); // { name: { value: "Rashi", writable: true, enumerable: true, configurable: false } }

// // Use console.log() to print data
delete puppy.name; // false

In this example, what we do is first we have to create a simple object and then seal it by calling Object.seal() then log its object descriptors by calling Object.getOwnPropertyDescriptors().

After all, we have tried to delete a property on it and returns false silently, which means JS can’t delete the Object, but If you run this code in the strict mode, you will get a TypeError.

03. Object.freeze()

This method is doing the same whatObject.seal()does. But the only difference here is all the object property's writable status has become false. This means you can't edit, re-configure its descriptors, or delete its properties anymore.

const puppy = {
name: 'Rashi'
};

Object.freeze(puppy);

// Use console.log() to print data
Object.getOwnPropertyDescriptors(puppy); // { name: { value: "Rashi", writable: true, enumerable: true, configurable: false } }

puppy.name = 'Rex';

// Use console.log() to print data
puppy.name; // "Rashi"

In the previous example, we have created a simple object then freeze Object by calling Object.freeze(). then log its object descriptors by calling Object.getOwnPropertyDescriptors().

After all, we have re-assign a value to puppy.name property and log its updated value to the console. But in our case object is frozen, which means you can’t re-write values; therefore JS engine remains its value as it is. But if you run this code in strict mode, you will get a TypeError as the same Object.seal()throws.

Object Copying (Cloning)

Here is one of the exciting parts of JS Objects. When it comes to object copying, there were two types we can identify. Which were Object Shallow Cloning and Deep Cloning. Both types carried out a different types of copying operations.

01. Shallow Cloning

Think if you want to create a variable that was a copy of another object which holds a specific data set and pointed to a different memory location. In that type of case, you can use shallow cloning.

const puppy = {
name: 'Rashi',
breed: 'German Shepard',
bark: 'Woof Woof!',
vaccinations: {
type: 1,
vaccine: 'parvo'
}
};

const shallowPuppy = puppy;

shallowPuppy.name = 'Rex' // overwrites reference object property value

// Use console.log() to print data
shallowPuppy.name; // "Rex"
puppy.name; // "Rex"

// Use console.log() to print data
shallowPuppy === puppy; // true
// Use console.log() to print data
shallowPuppy.name === puppy.name; // true
shallowPuppy.vaccinations === puppy.vaccinations; // true

Okay, now tell me, is this what we expected from the shallow copy? NOT EXACTLY. As I mention in the Object comparison section, all the literals and objects are pointing to the same object reference that our source object points, and not only that, all the inner objects and primitives are pointing to where source object properties pointed. That’s why changes are affected to the source object as well.

Now let’s see a graphical representation of this memory allocation.

Reference Copying — Graphical Demonstration

How can we avoid this and create an exact shallow copy of the puppy object instead of just assigning it? It’s a reference to another variable.

Using Object.assign()

This method can copy the values of all own enumerable properties from one or more source objects to a target object.

const puppy = {
name: 'Rashi',
breed: 'German Shepard',
bark: 'Woof Woof!',
vaccinations: {
type: 1,
vaccine: 'parvo'
}
};

const shallowPuppy = Object.assign({}, puppy);

// Use console.log() to print data
shallowPuppy === puppy; // false
// Use console.log() to print data
shallowPuppy.breed === puppy.breed; // true
shallowPuppy.vaccinations === puppy.vaccinations // true

Here, we create an object, then using Object.assign()we have assigned puppyobject properties to a target object and assign its reference to shallowPuppy variable.

Let’s take a look at the graphical representation for our base case scenario.

Object Shallow Copying — Sample Demonstration

You can see, as I explain from the beginning, shallow object and original object points to two distinct memory locations, and it contains non-primitive (object) is also pointing to the same reference actual object points, which means changes we do for a one-party will affects to the other party as well.

The same as the primitives, value-wise comparing it will return true, but the difference is that changes that will apply for a one-party will not affect the other party because each time we initialize or duplicate a literal property, it will automatically create and stores in a stack as a value.

Shallow cloning has consumes low memory resources compared to deep cloning because each object is pointing to the exact memory location its original object points.

02. Deep Cloning

Deep cloning is not like shallow cloning because the main object has a separate memory location. Not only that, each inner object has pointed to unique memory references without pointing where our source object inner non-primitives pointed. This means those inner objects will never share anything and works independently on their own.

Deep cloning has consumed higher memory resources than shallow cloning because each object points to different memory locations.

If you want to deep clone an object or an array without an effort, use JSON.stringify() and JSON.parse() to convert your whole object to a string and then again create an object with new reference allocations.

const puppy = {
name: 'Rashi',
breed: 'German Shepard',
vaccinations: {
type: 1,
vaccine: 'parvo'
}
};

let deepPuppy = JSON.stringify(puppy);
deepPuppy = JSON.parse(deepPuppy);

// Use console.log() to print data
deepPuppy === puppy; // false

// Use console.log() to print data
deepPuppy.name === puppy.name // false
deepPuppy === puppy.vaccinations // false

Let’s move to a sample graphical representation.

Object Deep Copying — Sample Demonstration

But keep in mind, this method is not a proper method of Deep cloning; however, it does the functionality we want, but you may refer to an article to gain adequate knowledge in the deep object cloning context.

In this article, I will not talk about many details about deep cloning. Some libraries support deep cloning functionality such as Lodash or try and implement yourself depending on your scenario 😉.

Object related Best Practices and Performance Factors

Here are a few of the best practices you can carry out when you dealing with JS objects.

PRACTICE 01 — As I said from the beginning, reduce the use ofnew keyword for construct your data types. always try to use following practices:

  • {} instead of new Object()
  • [] instead of new Array()
  • /())/ instead of new RegExp()
  • function () {} instead of new Function()

PRACTICE 02 — Always uses strict comparison other than comparing only the value of your literal or object.

Data Comparison — Sample Demonstration

PRACTICE 03 — Assign your data or methods to the object prototype if they re-use again and again for the same purpose. This optimization helps to improve your performance drastically other than duplicating the same code segment recursively.

Performance Impact of using Prototypes

--

--

Aravinda Rathnayake

Software Engineer | Technical Consultant | Diversity Visa Community Advisor