Objects and Prototypes in JavaScript

Object-oriented programming is a well-known programming paradigm. Object-oriented programming can be class-based or prototype-based. In prototype-based languages, objects exist, but classes do not. We will see in this article that JavaScript supports prototype-based object orientation.

Objects and Prototypes in JavaScript with yellow background

The density of information starts slows and ramps up, so beginners can get something out of the article as well. This article consists of the following sections:

  1. What is an Object?
  2. Prototypes in JavaScript
  3. JavaScript has no classes

1. What is an Object?

An object in JavaScript is a collection of properties. An object property, in its simple form, has a name (key) and a value. We use property names to point to values. All property names must be unique strings within the object, while a property value can be of any data type.

There are two common ways for creating objects, which are the literal syntax form and constructed form.

var obj1 = {};            // literal syntax
var obj2 = new Object();  // constructor syntax

We can create a property by giving it a value. Here we set our property with dot notation, but we could also use bracket notation which we do in the latter created object:

// dot notation
var person = new Object(); // Note that you could use {} instead of new Object();
person.name = "James";
person.age = 33;

// bracket notation
var person = new Object();
person["name"] = "James";
person["age"] = 33;

To access a value from an object, we can use the [] or . operator. We can also replace values in our object if the property name exists in the object. Here is where we utilize the key.

person.name;             // "James" - dot notation
person["name"]           // "James" - bracket notation
person.name = "Jessica"; // "James" is replaced with "Jessica"

We can also delete a property by using the delete operator, even though it's rarely done.

person.name;        // "Jessica"
delete person.name;
person.name;        // undefined

You might ask yourself, "well should I use bracket or dot notation?". Most often, we use the . notation because it's more readable. But depending on the situation, we might have to access the values differently. An example, when we would use bracket notation would be if you want to use a variable to access a value like var propertyName = "name", followed by person[propertyName]. Such an approach is not possible with dot notation.

What I just showed you is the basics, but we rarely actually setup an object by adding one property at a time. We almost always use literal syntax, which allows us to write our properties straight away:

var artist = {               // object
    name: "Michael Bolton",  // key "name" and value "Michael Bolton"
    age: 66,                 // key "age" and value 66 (number)
    occupation: "singer"     // key "occupation" and value "singer"
}

Now we have an artist object, which has three properties. If we look at name: "Michael Bolton" we can see that we have added a property with a key called "name" which points to the value "Michael Bolton".

1.1 Property Attributes & Property Descriptor

We have covered that a property has a name and a value. However, an object property has more attributes, like writable, enumerable, and configurable, which all default to true (boolean). Before ECMAScript 5, we could not change these attributes, but now with ECMAScript 2015, we may define our properties with a property descriptor.

Object.getOwnPropertyDescriptor returns a property descriptor, which allows us to see the description of a specific property:

var person = {
	age: 29
}

var descriptor = Object.getOwnPropertyDescriptor(person, "age");

console.log(descriptor);
// { 
//   value: 29, 
//   writable: true, 
//   enumerable: true, 
//   configurable: true 
// }

If we want to add or change a property and also have control of these attributes, we can use Object.defineProperty:

var person = {};

Object.defineProperty(person, "age", {
	value: 29,
	writable: true,
	enumerable: true,
   	configurable: true
});

1.1.1 Writable

If writable is true, then we may change the value. If it's false, we may not.

var person = {};

Object.defineProperty(person, "age", {
    value: 29,
    writable: false,
    enumerable: true,
    configurable: true,
});

person.age; // 29
person.age = 30;
person.age; // 29 - still 29

1.1.2 Enumerable

var person = {
	name: "carl",
	age: 20
};

for (key in person)
	console.log(key);

The following code would print name and age. Now if we put a property's attribute enumerable to false, it would make it non-enumerable. In the example below, it will only print age.

var person = {
    name: "carl",
    age: 20
};

// We redefine the name property's enumerability
Object.defineProperty(person, "name", {
    value: "carl",
    writable: true,
    enumerable: false,
    configurable: true,
});

for (key in person)
    console.log(key);

1.1.3 Configurable

If you do not want to be able to change the descriptor definition, you may set configurable to false.

var person = {
    name: "carl"
};

Object.defineProperty(person, "name", {
    value: "carl",
    writable: true,
    enumerable: true,
    configurable: false,
});

// We may now not redefine the property
Object.defineProperty(person, "name", {
    value: "carl",
    writable: true,
    enumerable: true,
    configurable: true,
});

This results in TypeError: can't redefine non-configurable property "name". See that writable is true, so we can still change the value by:

person.name = "phil";
person.name // "phil"

1.2 Accessor Properties - Getter and Setters

We can also define and retrieve values with getters and setters which we refer to as, accessor properties. This means that there are two types of properties, data properties (which we covered above) and accessor properties.

var person = {
    name: "james", // data property
    title: "sir",  // data property

    // accessor property
    get something () {},
    set something (value) {}
}

A property with a getter method makes it possible to read the property. A setter method makes it possible to write to the property. We can create our own getter by writing a function like shown below. No arguments get passed to the get method, the method itself gets invoked, when we try to get the value from the accessor property.

var person = {
    get name() {
        return "james"
    } 
}

person.name; // "james"

If we would set the value instead, then the setter method would be called, and the value we assign gets passed to the method.

var person = {
    get name() {
        return this.something;
    }, 
    set name(value) {
        this.something = value;
    } 
}

person.name = "james";
person.name; // "james"

We use this.something because else it would go on recursively forever. However, this is just to illustrate the basics. Perhaps you would be more interested in doing something like:

var person = {
    name: "james", 
    title: "sir", 

    get fullName () {
        return this.title + " " + this.name;
    } 
}

person.fullName; // "sir james"

2. Prototypes in JavaScript

A prototype in JavaScript is a property on an object that points to another object. Before we look more into what a prototype is, I would like to show you why we would want to use prototypes.

Right, so imagine the following code:

function Person (name)  {
    this.name = name;

    this.sayHello = function () {
        console.log(`Hello my name is ${this.name}`)
    }
}

let bill = new Person("Bill");
let carl = new Person("Carl");

console.log(bill); // { name: "Bill", sayHello: sayHello() }
console.log(carl); // { name: "Carl", sayHello: sayHello() }

bill.sayHello === carl.sayHello; // false

It's pretty straight forward, we have created two similar objects. But there's a big problem with this approach. The problem with this approach is that each time we create a new object, we also create a new function for the object's sayHello property. This takes up memory. Imagine, for example, creating hundreds of users objects like this, we would be inundated with functions that do the same thing.

A solution to this is to have another object store the function so we can use that object as a reference:

const person = {
    sayHello () {
        console.log(`Hello my name is ${this.name}`)
    }
}

let bill = Object.create(person);
bill.name = "Bill";

let carl = Object.create(person);
carl.name = "Carl";

bill.sayHello === carl.sayHello; // true

What we have done here is that by using Object.create we've created two new objects that is linked with the person object.

This might surprise you, but the bill object looks like this, { name: "Bill" } but we can still call sayHello. This is because the bill object actually has a property called prototype. As we've covered, a prototype is a property on an object that points to another object. The pointed object works as a fallback source of properties. Meaning, when we call bill.sayHello it is not found, so then bill.prototype gets looked at and there it's found.

This means that we can put functions directly on the prototype if we'd like to do a constructor call on a function to create a new object:

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

Person.prototype.sayHello = function () {
	console.log(`Hello my name is ${this.name}`);
}

let bill = new Person("Bill");
let carl = new Person("Carl");

bill.sayHello === carl.sayHello; // true

Now bill and carl will be two different objects, but they still point to the same prototype. The prototype has the sayHello function. Futhermore, since bill and carl are two different objects we can, for example, add a function we only want the bill object to have.

bill.dance = function () {
    console.log("I'm dancing");
}

console.log(bill); // { name: "Bill", dance: dance() }
console.log(carl); // { name: "Carl" }

2.1 Delegation & Prototype chain

Up until now, we have covered that prototypes work as a fallback, which is more specifically known as delegation. With delegation, we delegate the responsibility to another object. An object first checks itself “do I have this on me?” if not then it checks its "parent", and this would be repeated until a property is found or not.

var obj1 = {
    name: "james"
}

var obj2 = Object.create(obj1);
obj2.name = "bill";

var obj3 = Object.create(obj2);
obj3.name = "carl";

We have now created a prototype chain that looks like, obj1 <- obj2 <- obj3. Now let us delete one property at a time to illustrate delegation.

obj3.name; // carl
obj2.name; // bill
obj1.name; // james

delete obj3.name;
obj3.name; // bill
obj2.name; // bill
obj1.name; // james

delete obj2.name;
obj3.name; // james
obj2.name; // james
obj1.name; // james

3. JavaScript has no classes

Now we shift our focus to classes. ES6 gave us the class keyword, which allows us to use the following syntax:

class Person {
    constructor(name) {
         this.name = name;
    }

    sayHello () {
         console.log(`Hello my name is ${this.name}`);
    }
}

class Employee extends Person {
    constructor(name, position) {
         super(name);
         this.position = position;
    }

    work () {
        console.log("I'm working now");
    }
}

let bill = new Employee("Bill", "Senior Developer");
let carl = new Employee("Carl", "Junior Developer");

After seeing the code you might think, "why did you waste my time with prototypes?". Well, that's because it's all an illusion because there are no classes in JavaScript. This is because as written in the beginning, JavaScript uses prototype-based programming. But it looks like a class, right? Yes, it does, but it's only syntactic sugar. Before ES6 we would have to do something like this:

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

Person.prototype.sayHello = function () {
    console.log(`Hello my name is ${this.name}`);
}

function Employee (name, position) {
    Person.call(this, name);
    this.position = position;
}

Employee.prototype = Object.create(Person.prototype);

Employee.prototype.work = function () {
    console.log("I'm working now");
}

let bill = new Employee("Bill", "Senior Developer");
let carl = new Employee("Carl", "Junior Developer");

All of this is still very class-oriented. Another approach could be to use OLOO (objects linked to other objects) which is more focused on knowing how delegation works.

const Person = {

    init (name) {
       this.name = name;
    },

    sayHello () {
        console.log(`Hello my name is ${this.name}`);
    }
}

let Employee = Object.create(Person);

Employee.setup = function (name, position) {
   this.init(name);
   this.position = position;
}

Employee.work = function () {
   console.log("I'm working now");
}


let bill = Object.create(Employee);
bill.setup("Bill", "Senior Developer");

let carl = Object.create(Employee);
carl.setup("Carl", "Junior Developer");