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:
- JS searches inside the
car
object for a property namedstart
. - JS doesn't find
start
within thecar
object. - JS accesses the
car.__proto__
property. - JS has now a direct link to the prototype and starts the search in it.
- JS finds the property within the prototype and returns it.
- Since
start
is invoked as a method oncar
, the value ofthis
is set tocar
.
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