Classes


Constructor functions


Introduction

// Ctor function
function Automobile() {
    this.brand = 'BMW';
}

Rather than declaring local variables, constructor functions persist data with the this keyword. The above function will add a brand property to any object that it creates, and assigns it a default value of BMWM. Ctor functions should not have an explicit return value. The name of a ctor function, should be written with the first letter capitalized to visually distinguish it from a regular function.

Creating objects

// Using the JS keyword 'new'
let car1 = new Automobile();

// Using an object literal
let car2 = { brand: 'BMW'};

Ctor functions can have parameters

// Ctor function
function Automobile(model, color) {
    this.brand = 'Audi';
    this.model = model;
    this.color = color;
    this.description = function () {
        console.log(`${this.brand} ${this.model} ${this.color} car`);
    }
}

// Create objects
const car1 = new Automobile('TT', 'red');
const car2 = new Automobile('A4', 'green');

console.log(car1.model);
// TT

console.log(car2.color);
// green

car1.description();
// Audi TT red car

car2.description();
// Audi A4 green car

We can use the same constructor function to create as many objects as we'd like!

Seeing an object's ctor

What if we want to see if an object was created with a constructor function in the first place? We can use the instanceOf to give us some insight.

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

// Object
const woman = new Person('Mary');

// Check type
typeof woman;
// object

// Check if object is an instance of the 'Person' ctor function
woman instanceof Person;
// true

'this' keyword


In object methods

// Create object
const man = {
    name: 'Marios',
    sayHello: function () {
        return 'Hello';
    },
    greetings: function () {
        console.log(`${this.sayHello()} from ${this.name}`);
    }
};

// Call method
man.greetings();
// Hello from Marios

Using this, methods can access and manipulate an object's properties. In the above example, greetings() can use this to access the man object, which contains the sayHello() method.

In ctor functions

// Ctor function
function Person(name) {
    this.name = name;
    this.sayHello = function () {
        return 'Hello';
    };
    this.greetings = function () {
        console.log(`${this.sayHello()} from ${this.name}`);
    };
}

// Create object
const man = new Person('Marios');

// Call method
man.greetings();
// Hello from Marios

Calling the ctor function with the new keyword sets he value of this to the newly-created object.

Setting our own 'this'

'call()' method

function.call(thisArg, arg1, arg2, ...)
thisArg
Value to be set as this
arg1, arg2, ..., argn
Arguments of function()

Using call() to invoke a method, allows us to 'borrow' a method from one object and then use it for another object!


// Create object
const car1 = {
    brand: 'Audi',
    model: 'A4',
    color: 'blue',
    describe: function () {
        // 'this' refers to the 'car1' object
        console.log(`${this.brand} ${this.model} ${this.color} car`);
    }
};

// Create object
const car2 = {
    brand: 'VW',
    model: 'Golf',
    color: 'red'
};

car1.describe();
// Audi A4 blue car

car2.describe();
// Uncaught TypeError: car2.describe is not a function

car1.describe.call(car2);
// VW Golf red car

'apply()' method

function.apply(thisArg, [argsArray])
thisArg
Value to be set as this
[argsArray]
Arguments of function()

Note: While the syntax of this function is almost identical to that of call(), the fundamental difference is that call() accepts an argument list, while apply() accepts a single array of arguments.

'bind()' method

The value of this has some potential scope issues when callback functions are involved, and things can get a bit tricky.

// Function
function foo(callback) {
    // If a function is simply invoked, 'this' is set to 'window'
    callback();
    callback();
}

// Object
const dog = {
    age: 5,
    grow: function() {
        this.age += 1;
    }
};

dog.grow();

dog.age;
// 6

// Function call: Doesn't work as expected
foo(dog.grow);

// You may have expected the value to be 8
dog.age;
// 6

foo() calls callback() as a function rather than a method. As a result, this refers to the global object and not the dog object.

Note: If a constructor function is called with the new operator, the value of this is set to the newly-created object. If a method is invoked on an object, this is set to that object itself. And if a function is simply invoked, this is set to the global object: window.

So how can we make sure that this is preserved? One way to resolve this issue is to use an anonymous closure to close over the dog object:

// Function
function foo(callback) {
    callback();
    callback();
}

// Object
const dog = {
    age: 5,
    grow: function() {
        this.age += 1;
    }
};

// Function call
foo(function () {
    // If a method is invoked on an object, 'this' is set to that object itself
    dog.grow();
});

dog.age;
// 7

Since this is such a common pattern, JavaScript provides an alternate and less verbose approach: the bind() method.

The bind() Method

function.bind(thisArg [,arg1[,arg2[,argN]]])
thisArg
Value to be set as this
arg1[,arg2[,argN]]]
Optional. A list of arguments to be passed to the new function.

Regarding the previous example, an alternative and less verbose approach to preserved this, is to explicitly set the dog.grow()'s this value to the dog object:

// Function
function foo(callback) {
    // If a function is simply invoked, 'this' is set to 'window', unless the
    // function's 'this' value is explicitly set to another value
    callback();
    callback();
}

// Object
const dog = {
    age: 5,
    grow: function() {
        this.age += 1;
    }
};

// A copy of 'dog.grow()' with specified 'this' value
const growCopy = dog.grow.bind(dog);

// Function call
foo(growCopy);

dog.age;
// 7

Examples

Example 1: bind()

Consider the following driver and car objects:

const driver = {
    name: 'Danica',
    displayName: function () {
        console.log(`Name: ${this.name}`);
    }
};

const car = {
    name: 'Fusion'
};

Write an expression using bind() that allows us to 'borrow' the displayName() method from driver for the car object to use:

const displayNameCopy = driver.displayName.bind(car);

displayName();
// Name: Fusion

Example 2: call() & apply()

// Create object
const cow = {
    name: 'Dolly'
};

// Create function
function sayHello(message) {
    console.log(this);
    console.log(`${message}, ${this.name}`);
}

// 'this' refers to 'window' object
sayHello('Hello from');
// Window {...}
// Hello from,

// 'this' refers to 'cow' object
sayHello.call(cow, 'Hello from');
// {name: 'Dolly'}
// Hello from, Dolly

// 'this' refers to 'cow' object
sayHello.apply(cow, ['Hello from']);
// {name: 'Dolly'}
// Hello from, Dolly

Example 3: call()

Consider the following Andrew and Richard objects:

const Andrew = {
    name: 'Andrew',
    introduce: function () {
        console.log(`Hi, my name is ${this.name}!`);
    }
};

const Richard = {
    name: 'Richard',
    introduce: function () {
        console.log(`Hello there! I'm ${this.name}.`);
    }
};

Question: When Richard.introduce.call(Andrew); is executed, what is logged to the console?
Answer: 'Hello there!' I'm Andrew.'

Example 4: call()

Consider the following:

const andrew = {
    name: 'Andrew'
};

function introduce(language) {
    console.log(`I'm ${this.name} and my favorite programming language is ${language}.`);
}

Write an expression that uses the call() method to produce the message: 'I'm Andrew and my favorite programming language is JavaScript.'

Answer: introduce.call(andrew, 'JavaScript');

Prototypal inheritance


Introduction

Consider the following example:

// Ctor function
function Cat(name, age) {
    this.name = name;
    this.age = age;
    this.meow = function () {
        console.log(`Meow! My name is ${this.name}`);
    }
}

// Create objects
const keira = new Cat('Keira', 3);
const bob = new Cat('Bob', 4);

keira.meow();
// 'Meow! My name is Keira'

bob.meow();
// 'Meow! My name is Bob'

Let's go ahead and remove meow() from the Cat ctor and add it to its prototype:

// Ctor function
function Cat(name, age) {
    this.name = name;
    this.age = age;
}

// Create objects
const keira = new Cat('Keira', 3);
const bob = new Cat('Bob', 4);

// Ctor's function prototype: Note that even after we make the new objects,
// 'keira' & 'bob', we can still add properties to Cat's prototype
Cat.prototype.meow = function () {
    console.log(`Meow! My name is ${this.name}`);
};

keira.meow();
// 'Meow! My name is Keira'

bob.meow();
// 'Meow! My name is Bob'

When we called meow() on keira (and bob likewise), the JS engine will look first at the object's own properties to find a name that matches meow(). Since the method isn't directly defined in the object, it will then look at the object's ctor prototype for a match.

Note: In cases where the property doesn't exist in the prototype, the JS engine will continue looking up the prototype chain. At the very end of the chain is the Object() object, or the top-level parent. If the property still cannot be found, the property is undefined.

Note: Each function has a prototype property, which is really just an object. All objects created by a ctor function keep a reference to that prototype and can use its properties as their own!

Adding methods to the ctor funtion's prototype

To save memory and keep things DRY, we can add methods to the ctor function's prototype.

Consider the following two code snippets below:

// A
function Cat(name) {
    this.name = name;
    this.meow = function () {
        console.log(`Meow! My name is ${this.name}`);
    }
}
// B
function Cat(name) {
    this.name = name;
}

Cat.prototype.meow = function () {
    console.log(`Meow! My name is ${this.name}`);
}

Let's say that we want to define a method that can be invoked on instances (objects) of the Cat ctor function (we'll be instantiating at least 101 of them!).

Question: Which of the preceding two approaches is optimal?

Answer: (B) is optimal, because the function that meow points to does not need to be recreated each time a Cat object is created.

Replacing the prototype

// Ctor function
function Parrot(name) {
    this.name = name;
}

// Create objects
const charly = new Parrot('Charly');
const ricky = new Parrot('Ricky');

// Even after we make the new objects, 'charly' & 'ricky', we can still add
// properties to the prototype
Parrot.prototype.speak = function () {
    console.log('Hellooooooo');
};

charly.speak();
// 'Hellooooooo'

ricky.speak();
// 'Hellooooooo'

Now let's replace Parrot's prototype As we can see, the previous objects don't have access to the updated prototype's properties; they just retain their secret link to the old prototype:

// Replace 'prototype'
Parrot.prototype = {
    isCockatoo: true,
    color: 'white'
};

console.log(charly.color);
// undefined

console.log(ricky.isCockatoo);
// undefined

As it turns out, any new Parrot objects created moving forward will use the updated prototype:

// Create object
const erik = new Parrot('Erik');

erik.speak();
// TypeError: erik.speak is not a function

console.log(erik.color);
// 'white'

console.log(erik.isCockatoo)
// true

Standard built-in objects & their prototypes

Instances (objects) inherit from their ctor's prototype.

Ctors and their respective prototypes:

  • Array/Array.prototype
  • String/String.prototype
  • Number/Number.prototype
  • Boolean/Boolean.prototype
  • Date/Date.prototype
// Create 'Array' objects
var myArray1 = [1, 2, 3];
var myArray2 = ['a', 'b', 'c'];

// 'push()' is defined in the Array.prototype
myArray1.push(4);
// [1, 2, 3, 4]

// 'push()' is defined in the Array.prototype
myArra2.push('d');
// ['a', 'b', 'c', 'd']

We can change the constructor's prototype object (Array.prototype) to make changes to all Array instances (objects). For example, you can add new methods and properties to extend all Array objects.


Array.prototype.myReverse = function () {
    var res = new Array;
    for(let i = this.length - 1; i >= 0; i--) {
        res.push(this[i]);
    }
    return res;
}

myArray1 = myArray1.myReverse();
// [4, 3, 2, 1]

myArray2 = myArray2.myReverse();
// ['d', 'c', 'b', 'a']

Checking an object's properties

Introduction

If an object doesn't have a particular property of its own, it can access one somewhere along the prototype chain. In this section, we'll cover a few useful methods that can help us determine where a particular property is coming from.

'hasOwnProperty()' method

The hasOwnProperty() method returns a boolean indicating whether the object has the specified property as own (not inherited) property.

// Ctor
function Foo() {
    // Own property
    this.property1 = '1234';
}

// Inherited property
Foo.prototype.property2 = '5678';

// Crteate object
const myFoo = new Foo();

console.log(myFoo.hasOwnProperty('property1'));
// true

console.log(myFoo.hasOwnProperty('property2'));
// false

'isPrototypeOf()' method

The isPrototypeOf() method checks if an object exists in another object's prototype chain. Using this method, you can confirm if a particular object serves as the prototype of another object.

// Prototype
const motorVehicle = {
    move: function() {
        console.log('I am moving!');
    }
}

// Ctor function
function Car(model, displacement) {
    this.model = model;
    this.displacement = displacement;
}

// Replace Car's prototype
Car.prototype = motorVehicle;

// Create object
const myCar = new Car('Suzuki Jimny', 1.3);

// model: own property
console.log(myCar.model);
// 'Suzuki Jimny'

// move(): inherited property
myCar.move();
// I am moving!

// The prototype of a 'Car' instance is the 'motorVehicle' object
console.log(motorVehicle.isPrototypeOf(myCar));
// true

Note: isPrototypeOf() works well, but keep in mind that in order use it, you must have that prototype object at hand in the first place.

'Object.getPrototypeOf()' method

The Object.getPrototypeOf() method returns the prototype of the specified object.

Continuing from the previous example:

const myPrototype = Object.getPrototypeOf(myCar);

console.log(myPrototype);
// {move: ƒ}

Let's consider another example and let's say that we create the following object, capitals, using the literal notation:

const capitals = {
    California: 'Sacramento',
    Washington: 'Olympia',
    Oregon: 'Salem',
    Texas: 'Austin'
};

Question: What is returned when Object.getPrototypeOf(capitals); is executed?

Solution: Because the object was created using literal notation, its constructor is the built-in Object() constructor function which in turn maintains a reference to Object.prototype

Answer: A reference to Object()'s prototype.

Object.getPrototypeOf(capitals) === Object.prototype
// true

'constructor' property

Each time an object is created, a special property is assigned to it under the hood: constructor. Accessing an object's constructor property returns a reference to the constructor function that created that object in the first place!

// Ctor
function Longboard() {
    this.material = 'bamboo';
}

// Create object
const board = new Longboard();

// Print the ctor function that created the 'board' object
console.log(board.constructor);
// ƒ Longboard() {
//     this.material = 'bamboo';
// }

Keep in mind that if an object was created using literal notation, its constructor is the built-in Object() constructor function!

const foo = {
    description: 'foo object',
    message: 'hello world!'
}

console.log(foo.constructor);
// ƒ Object() { [native code] }

Prototypal inheritance: subclasses


Introduction

With inheritance we can subclass, that is, have a 'child' object take on most or all of a 'parent' object's properties while having unique properties of its own. The class from which the subclass is derived is often called a superclass.

Inheritance via prototypes

Introduction

Every object created with the Cat() ctor function, is linked to the ctor function's prototype. As such, the meow() method is inherited:

// Ctor function
function Cat(name) {
    this.name = name;
}

// Ctor function's prototype
Cat.prototype.meow = function () {
    console.log(`Meow! My name is ${this.name}`);
}

// Create objects
const cat = new Cat('Felix');

cat.name;
// 'Felix'

cat.meow();
// 'Meow! My name is Felix'

Secret link to prototype

// Object
const bigCat = {
    diet: 'carnivore',
    claws: 'true'
};

// Ctor function
function Tiger(name) {
    this.name = name;
}

// Ctor function's prototype is bigCat
Tiger.prototype = bigCat;

// Object
const tiger = new Tiger('Tony');

// Add properties
tiger.weight = 200;
tiger.favoriteFood = 'deer';

tiger has three properties of its own and also has access to properties that exist as properties in the prototype object: diet and claws.

// Own property
console.log(tiger.weight);
//200

// Own property
console.log(tiger.favoriteFood);
//"deer"

// Own property
console.log(tiger.name);
//"Tony"

// Inherited property
console.log(tiger.diet);
//"carnivore"

// Inherited property
console.log(tiger.claws);
//"true"

Right after an object is made from its ctor, it gets an immediate access to properties and methods in the ctor's prototype.

The secret link that leads to the prototype is __proto__, a property of all objects made by a ctor function, that points to the prototype object.

console.log(tiger.__proto__);
// {diet: "carnivore", claws: "true"}

console.log(tiger.__proto__ === bigCat);
// true

Note: Do not ever reassign the __proto__ property, or even use it in any code you write. Use Object.getPrototypeOf() instead.

Examples

Example 1

Consider the following:

// Ctor
function Car (mfr, model, color, year) {
    this.mfr = mfr;
    this.model = model;
    this.color = color;
    this.year = year;
}

// Add method to the prototype
Car.prototype.start = function() {
    console.log(`${this.model} started successfully!`);
}

// Object
const car = new Car('Ford', 'Mustang', 'white', 1965);

car.start();
// Mustang started successfully!

What happens when car.start(); is executed? List the events in the order that they occur:

  1. JS searches inside the car object for a property named start.
  2. JS doesn't find start within the car object.
  3. JS accesses the car.__proto__ property.
  4. JS has now a direct link to the prototype and starts the search in it.
  5. JS finds the property within the prototype and returns it.
  6. Since start is invoked as a method on car, the value of this is set to car.

Object.create()

Introduction

There is another way to manage inheritance without altering the prototype using Object.create().

What does Object.create() do is that it takes a single object as an argument, and returns a new object whose __proto__ property is set to whatever was originally passed into Object.create().

// amphibian object
const amphibian = {
    ectothermic: true,
    eat: function() {
        console.log("Yummy!");
    }
};

// frog object (extends amphibian)
const frog = Object.create(amphibian);

console.log(amphibian.ectothermic);
// true

amphibian.eat();
// Yummy!

console.log(frog.__proto__);
// {ectothermic: true, eat: ƒ}

frog is linked to amphibian. As a result, frog inherits all of the amphibian's properties.

Object.create() gives us a clean method of establishing prototypal inheritance in JavaScript.

Single inheritance

We can use Object.create() to have objects inherit from just about any object we want (i.e. not only the prototype).

Object.create() is a great way to implement prototypal inheritance without altering the prototype.

// "Animal" superclass
function Animal(name) {
    this.name = name;
}

// Superclass method
Animal.prototype.sleep = function() {
    console.log('zzzzzzzzzzzzzzzz');
}

// "Cat" subclass
function Cat(name) {
    Animal.call(this, name); // call super ctor
    this.mammal = true;
}

// "Cat" inherits from "Animal" (subclass extends superclass)
Cat.prototype = Object.create(Animal.prototype);

// "Cat"'s ctor points to "Animal" but must point to "Cat"
Cat.prototype.constructor = Cat;

// Subclass method
Cat.prototype.meow = function() {
    console.log('meow meow meow');
}

// Create an object
const cat = new Cat('Felix');

The cat object has all the properties and methods we can expect, either inherited or owned:

cat.name;
// "Felix"

cat.sleep();
// zzzzzzzzzzzzzzzz

cat.mammal;
// true

cat.meow();
// meow meow meow
Ad1
advertisement
Ad2
advertisement
Ad3
advertisement
Ad4
advertisement
Ad5
advertisement
Ad6
advertisement