DEV Community

Cover image for A Deep Dive into JavaScript Objects And Prototypes
Ashutosh Bhadoria
Ashutosh Bhadoria

Posted on • Edited on

A Deep Dive into JavaScript Objects And Prototypes

For anybody who has been working with JavaScript even on a beginner level, has come across the notion of object in one's code. Remember the first program in JavaScript we wrote, it must have looked like console.log('Hello World!'). Where we used the log method of the console object.

Broadly speaking, objects in JavaScript can be defined as an unordered collection of related data, of primitive or reference types. This data is represented in the 'key: value' form. The keys can be variables or functions, which in the context of objects are referred to as properties and methods.

Without further ado, lets create our first object by using the object literal.

var beer = {
  name: 'Guinness',
  style: 'Stout'
};
Enter fullscreen mode Exit fullscreen mode

As we can see we just created an object with a name of beer and two properties which are name and style, with values 'Guinness' and 'Stout' respectively. We can access these properties very easily by using the dot operator.

> console.log(beer.name);
  Guinness
> console.log(beer.style);
  Stout
Enter fullscreen mode Exit fullscreen mode

Once an object is created using an object literal we can easily add additional properties to it, let's try to add a color property to our beer object and assign a value of black to it.

beer.color = 'Black';
Enter fullscreen mode Exit fullscreen mode
> console.log(beer.color);
  Black
Enter fullscreen mode Exit fullscreen mode

Similar to adding properties, methods can be added to our beer object very easily. We'll add a makePersonHappy() method to our object.

beer.makePersonHappy = function() {
  console.log('Be happy, Good things come to those who wait.');
}

Enter fullscreen mode Exit fullscreen mode

Let's execute this method right away,

> beer.makePersonHappy();
  Be happy, Good things come to those who wait.
Enter fullscreen mode Exit fullscreen mode

Also, deleting properties (or methods) from your object is very simple with the use of delete keyword, let's have a look at it in the code

var beer = {
  name: 'Guinness',
  style: 'Stout',
  color: 'Black',
  makePersonParty: function() {
    console.log('Partyyyy!');
  }
};

delete beer.color;
delete beer.makePersonParty;
Enter fullscreen mode Exit fullscreen mode
> console.log(beer);
  {name: "Guinness", style: "Stout"}
Enter fullscreen mode Exit fullscreen mode

So, we can see the property color and the method makePersonParty are successfully deleted from our object beer.

Wait, where are the Classes ?

If you are coming from a background in statically typed languages (like I did), it's easy to get bamboozled here, what did I just do?
what is a beer? I haven't even defined a class beer.
The thing is, in a dynamically typed language we can skip the whole ceremony of creating the blueprints i.e. the classes or types ahead of time before we their instances aka. the objects.

Just create an object when you need one with the properties and methods you deem necessary. But another powerful feature of JavaScript objects is that you can change the entire shape of the object as and when you feel necessary. We created our beer object with two properties, name and style, later we felt that the beer needs to have a color, so we added a color property, similarly, we thought it would be good if our beer made a person happy, so that's what we did we added a method to our object makePersonHappy. This dynamic nature allows for more flexibility with less code and less constrainsts.

Now this may seem fun for small scripts, but, especially after JavaScript has become a mainstay in the server side development ecosystem as well, a burning question is, HOW THE HECK DO I WRITE COMPLEX SYSTEMS ?

We will explore features JavaScript provides to get some of the same benefits you can from statically typed languages.

Creating Objects

Using Constructor Functions

function Beer() {
  this.name = 'Guinness';
  this.style = 'Stout';  
}

var beer = new Beer();
Enter fullscreen mode Exit fullscreen mode
> console.log(beer);
  Beer {name: "Guinness", style: "Stout"}
Enter fullscreen mode Exit fullscreen mode

JavaScript provides a new keyword which followed by a function (constructor function) helps us create objects with the desired properties (and methods), without losing the dynamic nature of JavaScript objects. The constructor function is like any other JavaScript function with the first letter of it's name capitalized as a convention.

Let's just take a look at our new Beer object. There you can see that our lowercase beer variable is now a pointer to a Beer object, and that beer is named Guinness and is a Stout. So how exactly did that work? To really understand what is happening here, it's important that you understand what the keyword this is in JavaScript. The this keyword refers to an object. That object is whatever object is executing the current bit of code. By default, that is the global object. In a web browser, that is the window object. So when we executed this Beer function, what was this referring to? It was referring to a new empty object. That's what the new keyword does for us. It creates a new empty JavaScript object, sets the context of this to that new object, and then calls the Beer function. (If it doesn't make sense, please re-read this paragraph)

Let's now make out contructor function dynamic enough to create different beers.

function Beer (name, style) {
  this.name = name;
  this.style = style;
}

var guinness = new Beer('Guinness', 'Stout');
var miller = new Beer('Miller', 'American Pilsner');
Enter fullscreen mode Exit fullscreen mode
> console.log(guinness);
  Beer {name: "Guinness", style: "Stout"}
> console.log(miller);
  Beer {name: "Miller", style: "American Pilsner"}
Enter fullscreen mode Exit fullscreen mode

Using ECMAScript 6 Classes

class Beer {
  constructor (name, style) {
    this.name = name;
    this.style = style;
  }
}

var guinness = new Beer('Guinness', 'Stout');
var miller = new Beer('Miller', 'American Pilsner');
Enter fullscreen mode Exit fullscreen mode
> console.log(guinness);
  Beer {name: "Guinness", style: "Stout"}
> console.log(miller);
  Beer {name: "Miller", style: "American Pilsner"}
Enter fullscreen mode Exit fullscreen mode

ES6 classes offer a relatively cleaner and very similar syntax to create objects which may seem familiar to class declarations in statically typed languages.

Using Object.create()

So far we've seen three ways to create JavaScript objects - the object literal, contructor functions and ES6 classes. But there is another way to create objects and is actually how objects are created under the hood even when we use the syntactic sugar available in the three ways we saw earlier.

var guinness = Object.create(Object.prototype, {
  name: {
    value: 'Guinness',
    writable: true,
    iterable: true,
    configurable: true
  },
  style: {
    value: 'Stout',
    writable: true,
    iterable: true,
    configurable: true
  }
});
Enter fullscreen mode Exit fullscreen mode
> console.log(guinness);
  Beer {name: "Guinness", style: "Stout"}
> console.log(miller);
  Beer {name: "Miller", style: "American Pilsner"}
Enter fullscreen mode Exit fullscreen mode

Now all these properties while creating an object using Object.create() may seem very weird because most of the times we do not interact with them and they are oblivious to us, because the other ways of creating objects just abstracts us from that detail. But we'll have a look at them later.

Object Properties

We have already seen creating objects with properties in the previous section, but there's a lot to object properties than it meets the eye. So far we have discussed accessing object properties with the dot notation, but there is an alternative and in some cases an essential construct to access object properties, the bracket notation.

var beer = {
  name: 'Miller',
  style: 'American Pilsner'
}
Enter fullscreen mode Exit fullscreen mode
> console.log(beer.name) // accessing properties using dot notation
  Miller

> console.log(beer['name']) // accessing properties using bracket notation
  Miller
Enter fullscreen mode Exit fullscreen mode

Just place the property name as a string (notice the single quotes) inside a bracket and we have an alternate syntax to aaccess object's properties.

What if we name our properties (or a data fetched as JSON from some source) which are not valid identifier names, in that case the dot notation will not work and we will have to use the bracket notation

var beer = {
  'beer name': 'Kingfisher' // property name is invalid identifier
}
Enter fullscreen mode Exit fullscreen mode
> console.log(beer['beer name'])
  Kingfisher
Enter fullscreen mode Exit fullscreen mode

Bracket notation is extremly useful when we want to access a property through a variable as a key.

var beerStyleKey = 'style';

var beer = {
  name: 'Hoegarden',
  style: 'Belgian Wheat Beer'
}
Enter fullscreen mode Exit fullscreen mode
> console.log(beer[beerStyleKey]) // accessing the property
                                  // using variable as a key
  Belgian Wheat Beer
Enter fullscreen mode Exit fullscreen mode

Property Descriptors

Let us take a closer look at properties, they are more than a key-value pair, using Object.getOwnPropertyDescriptor() which returns a property descriptor for an own property. (we will look at the difference between an own property and a prototype property later).

var beer = {
  name: 'Guinness',
  style: 'Stout'
}
Enter fullscreen mode Exit fullscreen mode
> Object.getOwnPropertyDescriptor(beer, 'name');
  {value: "Guinness", writable: true, enumerable: true, configurable: true}
Enter fullscreen mode Exit fullscreen mode

Now, in the output, we can see in addition to the property having a value, it also has writable, enumerable and configurable attributes.

Writable Attribute

The writable attribute controls wether we can change the value of the property from the initial value.

For demonstrating this behaviour we are going to use the JavaScript strict mode, and we are going to use Object.defineProperty() which defines a new property directly on an object, or modifies an existing property on an object, and returns the object.

Consider our object beer

'use strict';

var beer  = {
  name: 'Guinness',
  style: 'Stout'
};

// set the writable attribute for property style to false.
Object.defineProperty(beer, 'style', {writable: false});

Enter fullscreen mode Exit fullscreen mode
// try to change the style value for beer
> beer.style = 'Belgian Blond Beer';
  Uncaught TypeError: Cannot assign to read only property 'style' of object '#<Object>'
Enter fullscreen mode Exit fullscreen mode

As expected trying to reassign a new value to style property results in a TypeError being thrown.

A word of caution the key concept here is we will not be able to REDECLARE a property. So if in case, the property is an object, we can still modify that object, but we cannot set it to other object.

'use strict';

var beer = {
  name: 'Simba',
  placeOfOrigin: {
    city: 'Bangalore',
    country: 'India'
  }
}

Object.defineProperty(beer, 'placeOfOrigin', {writable: false});

beer.placeOfOrigin.city = 'Mumbai'; // works fine
beer.placeOfOrigin = {city: 'Moscow', country: 'Russia'}; // throws TypeError
Enter fullscreen mode Exit fullscreen mode

Enumerable Attribute

Whenever we want to list or print all the properties of an object we just throw in a good ol' for...in loop. By default, properties on an object are enumerable, meaning we can loop over them using a for…in loop. But we can change that. Let's set enumerable to false for the style property.

'use strict';

var beer  = {
  name: 'Guinness',
  style: 'Stout'
};

Object.defineProperty(beer, 'style', {enumerable: false});

for (var key in beer) {
  console.log(`${key} -> ${beer[key]}`);
}
Enter fullscreen mode Exit fullscreen mode
// output
name -> Guinness
Enter fullscreen mode Exit fullscreen mode

Well looks like our style property wasn't enumerated (no pun intended).

Setting the enumerable attribute to false also has another important implication, the JSON serialization of the object. Let's have a look what happens to our beer object which has enumerable attribute for style set to false.

> JSON.stringify(beer);
  "{"name":"Guinness"}"
Enter fullscreen mode Exit fullscreen mode

We didn't get the style property in our stringified object.

A convenient way to get all the keys (or attributes) of an object is to use the Object.keys() method, let's see what if we set enumerable attribute to false for a particular key.

> Object.keys(beer);
  ["name"]
Enter fullscreen mode Exit fullscreen mode

Again the only key showing up is the name key and not the style key.

Although we cannot enumerate the style key in the for...in loop, or JSON stringification, or in Object.keys(), we still have it present on the object. Let's print out it's value.

> console.log(beer.style);
  Stout
Enter fullscreen mode Exit fullscreen mode

Configurable attribute

The configurable attribute helps you lock down some property from being changed. It prevents the property from being deleted.

Lets see this in the code

'use strict';

var beer = {
  name: 'Guinness',
  style: 'Stout'
}

Object.defineProperty(beer, 'style', {configurable: false});
Enter fullscreen mode Exit fullscreen mode
// try deleting the style property.
> delete beer.style;
  Uncaught TypeError: Cannot delete property 'style' of #<Object>
Enter fullscreen mode Exit fullscreen mode

Also, after setting configurable attribute to false we cannot change the enumerable attribute of the object.

> Object.defineProperty(beer, 'style', {enumerable: false});
  Uncaught TypeError: Cannot redefine property: style
Enter fullscreen mode Exit fullscreen mode

Interestingly, once we set configurable atribute to false, we cannot flip it back to true.

> Object.defineProperty(beer, 'style', {configurable: true});
  Uncaught TypeError: Cannot redefine property: style
Enter fullscreen mode Exit fullscreen mode

However, note that we can still change the writable attribute on the style property.

Getters and Setters in JavaScript

Getters and Setters are properties on an object that allow you to set the value of a property or return the value of property using a function. Thus, allowing for a more secure and robust way of assigning or retrieving values of object properties.

var beer = {
  brand: 'Miler',
  type: 'Lite'
}
Enter fullscreen mode Exit fullscreen mode

Now suppose we wanted to retrieve the full name of our beer as 'Miller Lite' we could define a getter as follows,

var beer = {
  brand: 'Miller',
  type: 'Lite'
}

Object.defineProperty(beer, 'fullBeerName', {
  get: function() {
    return `${this.brand} ${this.type}`
  }
});
Enter fullscreen mode Exit fullscreen mode

Now let's see if our code works

> console.log(beer.fullBeerName);
  Miller Lite
Enter fullscreen mode Exit fullscreen mode

Well it does 😄

What if we wanted to do the reverse of what we've done, that we could supply a value such as 'Miller Lite' and it will set the brand property to 'Miller' and type property to 'Lite'. For this we need to define a setter.

var beer = {
  brand: 'Miller',
  type: 'Lite'
}

Object.defineProperty(beer, 'fullBeerName', {
  get: function() {
    return `${this.brand} ${this.type}`
  },
  set: function(str) {
    var parts = str.split(' ');
    this.brand = parts[0];
    this.type = parts[1];
  }
});
Enter fullscreen mode Exit fullscreen mode

Let's test this out,

> beer.fullBeerName = 'Kingfisher Strong';
> console.log(beer);
  {brand: "Kingfisher", type: "Strong"}
Enter fullscreen mode Exit fullscreen mode

It seems to work! We just set the brand and type property using a single assignment to fullBeerName.

Prototypes

Before we define and discuss prototypes let's consider an example, suppose we want to have a property which could give us last element of the array we defined. But as JavaScript is a dynamic language we can add a new property to achieve this.

var beers = ['Heineken', 'Miller', 'Tuborg'];

Object.defineProperty(beers, 'last', {
  get: function() {
    return this[this.length - 1];
  }
});
Enter fullscreen mode Exit fullscreen mode
> console.log(beers.last);
  Tuborg
Enter fullscreen mode Exit fullscreen mode

NOTE: Declaring and initializing arrays like we have done above by using the square bracket notation is just a simplified syntax for creating an Array using the Array constructor, var beers = ['Heineken', 'Miller', 'Tuborg']; is equivalent to var beers = new Array('Heineken', 'Miller', 'Tuborg');

However, the problem in this approach is, if we decide to define a new array we will need to define the last attribute again for that particular array. This approach is not extensible for all arrays.

If we define our last method on Array's prototype instead of the beers array we declared we will be able to achieve the expected behaviour.

Object.defineProperty(Array.prototype, 'last', {
  get: function () {
    return this[this.length - 1];
  }
});
Enter fullscreen mode Exit fullscreen mode
> var beers = ['Heineken', 'Miller', 'Tuborg'];
> console.log(beers.last);
  Tuborg
> var gins = ['Bombay Sapphire', 'Gordon', 'Beefeater'];
> console.log(gins.last);
  Beefeater
Enter fullscreen mode Exit fullscreen mode

Awesome.

So What is a Prototype?

A prototype is an object that exists on every function in JavaScript. Caution, some convoluted definitions are coming up. A function's prototype is the object instance that will become the prototype for all objects created using this function as a constructor. An object's prototype is the object instance from which the object is inherited.

Let's have a look at these concepts through code.

function Beer (name, style) {
  this.name = name;
  this.style = style;
}

var corona = new Beer ('Corona', 'Pale Lager');
Enter fullscreen mode Exit fullscreen mode
> Beer.prototype;
  Beer {}

> corona.__proto__;
  Beer {}

> Beer.prototype === corona.__proto__;
  true
Enter fullscreen mode Exit fullscreen mode

In the above example, when we define the constructor function Beer a protype object is created. Then we create a corona object using the Beer constructor function we can see that the same prototype object instance is available in the corona object (the name of the prototype object instance is __proto__ in case of the objects created from the constructor).

Let's tinker around with this prototype object.

Beer.prototype.color = "Golden";
Enter fullscreen mode Exit fullscreen mode
> Beer.prototype;
  Beer { color: 'golden' }

> corona.__proto__;
  Beer { color: 'golden' }

> console.log(corona.color);
  "Golden"

> var guinness = new Beer('Guinness', 'Stout');
> guiness.color;
  "Golden"
Enter fullscreen mode Exit fullscreen mode

We added a new property color to Beer's prototype and because the objects created from the Beer constructor have the exact same prototype object instance, the changes in function's prototype object are reflected in corona object's __proto__ object. Also, we can see another more practical effect of adding a property to the prototype object, we are able to access color property from all the objects created through Beer constructor using the simple dot notation. Let's discuss this in the next section.

Instance and Prototype properties

Let us code up our previous example real quick

function Beer (name, style) {
  this.name = name;
  this.style = style;
}

Beer.prototype.color = 'Black';

var guinness = new Beer('Guinness', 'Stout');
Enter fullscreen mode Exit fullscreen mode

Now we'll head to our JavaScript console to draw some insights from the above example

> (console.log(guinness.name);
  "Guinness"

> console.log(guinness.style);
  "Stout"

> console.log(guinness.color);
  "Black"
Enter fullscreen mode Exit fullscreen mode

So far so good, we are getting expected values for all the three properties.

Just to be sure, let us list the properties of the guinness object.

> Object.keys(guinness);
   ["name", "style"]
Enter fullscreen mode Exit fullscreen mode

Wait what? Where is the color property we just accessed it's value. Let's double-check this.

> guinness.hasOwnProperty('name');  // expected
  true

> guinness.hasOwnProperty('style'); // expected
  true

> guinness.hasOwnProperty('color') // Oh! Weird
  false
Enter fullscreen mode Exit fullscreen mode
> guinness.__proto__.hasOwnProperty('color'); // Hmmmm
  true
Enter fullscreen mode Exit fullscreen mode

To explain this, name and style are the properties of the guinness object and are referred to as Instance properties, while color is a Prototype property.

While trying to access a property of an object (using the dot or the square bracket notation) the engine first checks if the property we are trying to access is an Instance property, if yes the value of the Instance property is returned. However, when the property is not found in the Instance properties of the object, a look up of Prototype properties is performed, if a corresponding matching property is found, it's value is returned.

Let's see one last example to drive this concept home.

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

Beer.prototype.name = 'Kingfisher';

var corona = new Beer('Corona');
Enter fullscreen mode Exit fullscreen mode
> console.log(corona.name);
  "Corona"
Enter fullscreen mode Exit fullscreen mode

Even though the name property is available on the prototype it's value is not returned because first a look up of Instance properties is performed, where the property name was found and it's value of "Corona" is returned.

Multiple Levels of Inheritance

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

var corona = new Beer('Corona');
Enter fullscreen mode Exit fullscreen mode

We know now, that corona has a prototype and that it was created from the Beer function, as can be seen here.

> corona.__proto__;
  Beer {}
Enter fullscreen mode Exit fullscreen mode

But on close inspection we will see that the the Beer prototype also has a prototype.

> corona.__proto__.__proto__;
  Object {}    // maybe represented as `{}` in some environments
Enter fullscreen mode Exit fullscreen mode

This indicated that Beer objects inherit from Object. Let us try going up the prototype chain.

> corona.__proto__.__proto__.__proto__;
  null
Enter fullscreen mode Exit fullscreen mode

Looks like we've hit the roof. So to conclude this discussion, by default, all objects in JavaScript inherit from Object. And Object has no prototype. So almost all objects that we work with have some type of prototypal inheritance chain like this.

Creating Prototypal Inheritance Chains

To create complex systems it is often essential we think of in terms of creating ample abstractions to make the system design cleaner, robust and reusable.

Let us try to create an abstraction for our Beer class, let us say Beer is a type of Beverage, and the Beverage happens to make people happy. So, we add a method to Beverage's prototype makePersonHappy(). Now Beer being a Beverage should also be able to make people happy, right? Let us see how we can achieve this

function Beverage() {
}

Beverage.prototype.makePersonHappy = function () {
  console.log('You are feeling so good!');
}

function Beer (name, style) {
  this.name = name;
  this.style = style;
}

Beer.prototype = Object.create(Beverage.prototype);

var guinness = new Beer('Guinness', 'Stout');
Enter fullscreen mode Exit fullscreen mode

Let's see if guinness can make a person happy.

> guinness.makePersonHappy();
  "You are feeling so good!"
Enter fullscreen mode Exit fullscreen mode

So what happened was, when we defined the method makePersonHappy() on Beverage's prototype, every object created from the Beverage function would have this method. If you look closely at the line of code

Beer.prototype = Object.create(Beverage.prototype);
Enter fullscreen mode Exit fullscreen mode

This sets up a prototype chain from Beer to it's parent Beverage and therefore we are able to access the method makePersonHappy(). Let's verify this claim

> console.log(guinness.__proto__.__proto__);
  Beverage { makePersonHappy: [Function] }
Enter fullscreen mode Exit fullscreen mode

There is, however, one discrepancy here, let's print the guinness object.

> console.log(guinness);
  Beverage { name: 'Guinness', style: 'Stout' }
Enter fullscreen mode Exit fullscreen mode

Here the object guinness has Beverage as it's constructor, but we created this object using Beer function. Turns out we had overwritten the constructor property of the Beer's prototype when we established the prototype chain. This can be easily amended by explicitly setting the constructor property of the prototype.

Beer.prototype = Object.create(Beverage.prototype);
// explicitly setting the constructor
Beer.prototype.constructor = Beer;
Enter fullscreen mode Exit fullscreen mode

Now, let's go to the console to verify this

> console.log(guinness);
  Beer { name: 'Guinness', style: 'Stout' }
Enter fullscreen mode Exit fullscreen mode

A lot of times we may decide to change some default behaviour provided by the parent to better suit the design of the system. Here we will try to override the message shown in makePersonHappy() method provided by the Beverage. Let us use every thing we have covered in this sub-section.

function Beverage (message) {
  this.message = message || 'You are feeling so good!';
}

Beverage.prototype.makePersonHappy = function () {
  console.log(this.message);
}

function Beer (name, style) {
  // Call Beverage constructor
  Beverage.call(this, 'You have never felt better before!');
  this.name = name;
  this.style = style;
}

// Set prototype chain
Beer.prototype = Object.create(Beverage.prototype);
// Explicitly set constructor
Beer.prototype.constructor = Beer;

var guinness = new Beer('Guinness', 'Stout');
Enter fullscreen mode Exit fullscreen mode

In order to call the Beverage constructor we use the JavaScript's call method which calls a function with a given this value and arguments provided individually. This is done to take care of any initializations that we intended to do in the parent class, in this case we want to display a custom message from the makePersonHappy() method.

Let's verify if everything works fine.

> guinness.makePersonHappy();
  "You have never felt better before!"

> guinness;
  Beer {
    message: 'You have never felt better before!',
    name: 'Guinness',
    style: 'Stout'
  }
Enter fullscreen mode Exit fullscreen mode

Using Class Syntax to Create Prototype Chains

The way to achieve prototypal inheritance using the moden ES6 class syntax is very similar and perhaps more cleaner than what we have seen. Recall how in an earlier section we created objects from classes, let's apply those concepts here.

class Beverage {
  constructor (message) {
    this.message = message || 'You are feeling so good!';
  }

  makePersonHappy () {
    console.log(this.message);
  }
}

// Set up inheritance chain
class Beer extends Beverage {
  constructor (name, style) {
    // Call constructor of parent class
    super('You have never felt better before!');
    this.name = name;
    this.style = style;
  }
}

var guinness = new Beer('Guinness', 'Stout');
Enter fullscreen mode Exit fullscreen mode

Here we use the extends keyword to set up the inheritance chain, and used the super keyword to call parent class' constructor.
Let's test this out.

> guinness.makePersonHappy();
  "You have never felt better before!"

> console.log(guinness);
  Beer {
    message: 'You have never felt better before!',
    name: 'Guinness',
    style: 'Stout'
  }
Enter fullscreen mode Exit fullscreen mode

Note that here we did not have to explicitly set the constructor of the Beer's prototype.

Summary

With this deeper understanding, we'll be able to create powerful and well-structured applications that take advantage of the dynamic power of JavaScript to create real-world apps tackling complexity and standing the test of the harsh production-environments.

Happy coding 😎

Top comments (0)