Object Oriented Programming (OOP)

Background

  • OOP is a model based on objects constructed from a blueprint. We use OOP to write more modular and shareable code
  • In languages that have built-in support for OOP, we call these blueprints "classes" and the objects created from them "instances"
  • Since we do not have built-in class support in JavaScript, we mimic classes by using functions. These constructor functions create objects through the use of the new keyword
  • We can avoid duplication in multiple constructor functions by using call or apply

Constructor Function

  • Used as a blueprint
  • Capitalize the function name - this is convention!
  • The value of the keyword 'this' inside the constructor function will be the global object
  • Inside the constructor, we use the keyword 'this' to refer to the object we will create from our constructor function
  • no return statement
In [1]:
function Dog(name, age){
  this.name = name;
  this.age = age;
  this.bark = function(){
  console.log(this.name + " just barked!");
  }
}

Creating an object: new keyword

  • With the 'new' keyword which must be used with a constructor function
  • What does the 'new' keyword do? 1) Creates an empty object out of thin air 2) Assigns the value of 'this' to be that object 3) Adds 'return this' to the end of the function 4) It adds a property onto the empty object called __proto__ (aka dunder). 5) Dunder proto links the prototype property on the constructor function to the empty object (more on this later) Object.__proto__ === Constructor.prototype
In [2]:
var rusty = new Dog("Rusty", 10);

rusty.name;
Out[2]:
'Rusty'
In [3]:
rusty.age;
Out[3]:
10
In [4]:
rusty.bark();
Rusty just barked!

Multiple Constructors & Refactoring

  • We can refactor our code using call or apply or arguments
  • The only difference between call and apply is the 2nd parameter pass to the function.
  • We don't even need to pass in parameters by using 'arguments' keyword which is a list of all arguments that are passed to a function

Constructor function

In [5]:
function Car(make, model, year){
  this.make = make;
  this.model = model;
  this.year = year;
  this.numWheels = 4;
}

Refactor: using call

In [6]:
function Motorcycle(make, model, year){
  // borrow properties from Car
  Car.call(this, make, model, year)
  this.numWheels = 2;
}

Refactor: using apply

In [7]:
function Motorcycle(make, model, year){
  Car.apply(this, [make,model,year]);
  this.numWheels = 2;
}

Refactor: even better using apply with arguments

  • we don't need to even pass in parameters!
In [8]:
function Motorcycle(){
  Car.apply(this, arguments);
  this.numWheels = 2;
}

Exercise: Constructor Function

PART 1

Create a constructor function for a Person, each person should have a firstName, lastName, favoriteColor and favoriteNumber. Your function MUST be named Person.

In [9]:
function Person(firstName, lastName, favoriteColor, favoriteNumber){
  this.firstName = firstName;
  this.lastName = lastName;
  this.favoriteColor = favoriteColor;
  this.favoriteNumber = favoriteNumber;
}

Add a method called multiplyFavoriteNumber to the constructor function that takes in a number and returns the product of the number and the object created from the Person functions' favorite number.

In [10]:
function Person(firstName, lastName, favoriteColor, favoriteNumber){
  this.firstName = firstName;
  this.lastName = lastName;
  this.favoriteColor = favoriteColor;
  this.favoriteNumber = favoriteNumber;
  this.multiplyFavoriteNumber = function(num){
    return num * this.favoriteNumber;
  }
}

PART 2

Given the following code:-

function Parent(firstName, lastName, favoriteColor, favoriteFood){
  this.firstName = firstName;
  this.lastName = lastName;
  this.favoriteColor = favoriteColor;
  this.favoriteFood = favoriteFood;
}

function Child(firstName, lastName, favoriteColor, favoriteFood){
  this.firstName = firstName;
  this.lastName = lastName;
  this.favoriteColor = favoriteColor;
  this.favoriteFood = favoriteFood;
}

Refactor the Child function to remove all the duplication from the Parent function. You should be able to remove 4 lines of code in the Child function and replace it with 1 single line.

if use call, have to pass in all of this arguments separted by commas

In [11]:
function Child(){
  Parent.apply(this, arguments);
}

Prototypes: Basics

  • Every constructor function has a property on it called "prototype", which is an object
  • The prototype object has a property on it called "constructor", which points back to the constructor function
  • Anytime an object is created using the 'new' keyword, a property called "__proto__" gets created, linking the object and the prototype property of the constructor function

    prototype: found in constructor function __proto__: found in created object

img

Peson.prototype.constructor === Person
elie.__proto__ === Person.prototype

This is the constructor function

In [13]:
function Person(name){
  this.name = name;
}

In Javascript, every function has a property on it called prototype (auto created) Person.prototype

Create 2 new objects from the Person constructor

In [14]:
var elie = new Person("Elie"); //__proto__ is created from new keyword
var colt = new Person("Colt");

The 'new' keyword will add __proto__ to the object created. Dunder proto will point to the prototype property of the Person constructor

In [16]:
elie.__proto__ === Person.prototype;
Out[16]:
true
In [17]:
colt.__proto__ === Person.prototype;
Out[17]:
true

The Person.prototype object has a property called constructor which points back to the function

In [18]:
Person.prototype.constructor === Person;
Out[18]:
true

Prototype Chain

  • Prototype contains properties and methods that are shared and accessible by any object that is created from the constructor
  • Prototype Chain describes the way JS find methods and properties in an object
    • first find from object, then find from __proto__ (Array), then goes up the prototype chain to find next __proto__ (Object)
    • if not found, JS returns undefined
  • In Javascript, every object has a method called hasOwnProperty, where is this method located?

img

remember arr.proto === Array.prototype

In [19]:
var arr = []; //short hand of
var arr = new Array; //Array: built-in constructor function
arr.push; //where is the method located?
console.dir(arr); //find in __proto__ of Array
[]
In [20]:
arr.hasOwnProperty('length');
Out[20]:
true
dir(arr); //find in __proto__ of Object

Prototype Adding Methods

  • Defining class methods in the constructor is bad because of the cost of redefining a function in each object created
  • To share properties and methods for objects created by a constructor function, place them in the prototype as it is the most efficient
  • You can break working built-in methods if you add prototype properties that are the same name as existing ones (this is called monkey patching). It's also recommended that you do not add properties to the Object.prototype as that can have some pretty bad side effects.
In [22]:
function Vehicle(make, model, year){
  this.make = make;
  this.model = model;
  this.year = year;
  this.isRunning = false;
}

Put methods inside prototype

In [23]:
Vehicle.prototype.turnOn = function(){
  return this.isRunning = true;
}

Vehicle.prototype.turnOff = function(){
  return this.isRunning = false;
}

Vehicle.prototype.honk = function(){
  if (this.isRunning){
    return "beep";
  }
}
Out[23]:
[Function]

Exercise: Prototypes

1 - Create a constructor function for a Person. Each person should have a firstName, lastName, favoriteColor, favoriteNumber)

In [24]:
function Person(firstName, lastName, favoriteColor, favoriteNumber){
  this.firstName = firstName;
  this.lastName = lastName;
  this.favoriteColor = favoriteColor;
  this.favoriteNumber = favoriteNumber;
}

2 - Add a function on the Person.prototype called fullName that returns the firstName and lastName property of an object created by the Person constructor concatenated together.

Examples:

var person = new Person("Elie", "Schoppik", "purple", 34)
person.fullName() // "Elie Schoppik"
In [25]:
Person.prototype.fullName = function(){
  return this.firstName + " " + this.lastName;
}
Out[25]:
[Function]

3 - Add a property on the object created from the Person function called family which is an empty array. This will involve you adding an additional line of code to your Person constructor.

this.family = []: not add to prototype object because we don't want this to be shared by every object created by the Person constructor function when the new keyword is used

In [26]:
function Person(firstName, lastName, favoriteColor, favoriteNumber){
  this.firstName = firstName;
  this.lastName = lastName;
  this.favoriteColor = favoriteColor;
  this.favoriteNumber = favoriteNumber;
  this.family = [];
}

4 - Add a function on the Person.prototype called addToFamily which adds an object constructed from the Person constructor to the family array. To make sure that the object you are adding is an object construced from the Person constructor (HINT - take a look at the instanceof keyword). Make sure that your family array does not include duplicates! This method should return the length of the family array.

Examples:

var person = new Person("Elie", "Schoppik", "purple", 34)
var anotherPerson = new Person()
person.addToFamily(anotherPerson); // 1
person.addToFamily(anotherPerson); // 1
person.family.length // 1

person.addToFamily("test"); // 1
person.addToFamily({}); // 1
person.addToFamily([]); // 1
person.addToFamily(false); // 1
person.family.length // 1 */
In [28]:
Person.prototype.addToFamily = function(){
  if (arguments[0] instanceof Person){
    this.family.push(arguments[0]);
  }
  return this.family.length;
}

Person.prototype.addToFamily = function(person){
  //this.family.indexOf(person) === -1: ensure person not in the family array

  if(this.family.indexOf(person) === -1 && person instanceof Person){
    this.family.push(person)
  }
  return this.family.length;
}
Out[28]:
[Function]

PART II

1 - Implement your own version of Array.prototype.map. The function should accept a callback and return a new array with the result of the callback for each value in the array.

In [29]:
Array.prototype.map = function(callback){
  var newArr = [];
  for(var i = 0; i < this.length; i++){
    newArr.push(callback(this[i], i, this))
  }
  return newArr;
}
Out[29]:
[Function]

To use the function

In [30]:
[1,2,3].map(function(val){
  return val;
});
Out[30]:
[ 1, 2, 3 ]

2 - Implement a function called reverse that reverses a string and place it on the String.prototype

Examples:

"test".reverse() // "tset"
"tacocat".reverse() // "tacocat"*/
In [31]:
String.prototype.reverse = function(){
  return this.split("").reverse().join("");
}
Out[31]:
[Function]

Alternative

In [33]:
String.prototype.reverse = function(){
  var newStr = "";
  //loop backwards from the last char of string
  for (var i=this.length-1; i>=0; i--){
    newStr += this[i];
  }
  return newStr;
}
Out[33]:
[Function]

Inheritance

  • Inheritance is a process of the passing methods and properties from one class to another
  • JS is not OOP and mimic inheritance with objects and functions just like we do with constructor functions
  • We don't actually pass one constructor to another. We pass the prototype property of one constructor to another constructor
  • Every prototype has a property called constructor which points back to the constructor function i.e. Obj.prototype.constructor === Obj
  • obj.create overwrite the constructor property
  • Implementation: Step 1: Set the prototype to be an object created with another prototype (Object.create) Step 2: Reset the constructor property

Object reference <> Inheritance

  • We can't assign one object to another Obj1 = Obj2 - it will just create a reference!
  • No new objects is created here! obj2 is just a link to obj1
  • This means that if we change the prototye (properties & methods) of Obj1, it will affect prototype of Obj2
In [35]:
var parent = {name: "Elie"};
var child = parent;
child.name = "Tim";
Out[35]:
'Tim'
In [36]:
parent.name
Out[36]:
'Tim'

The correct way...

  • Step 1: Use object.create
    • to set the prototype to be an object created with another prototype
  • Step 2: Reset the constructor property

Parent constuctor function

In [37]:
function Person(firstName, lastName){
  this.firstName = firstName;
  this.lastName = lastName;
}

Child constructor function

In [38]:
function Student(firstName, lastName){
  Person.apply(this, arguments);
}

Create object by copying prototype which overwrite the prototype.constructor` of Student!

In [39]:
Student.prototype = Object.create(Person.prototype);
Out[39]:
Person {}

Student link to person constructor (wrong!)

In [41]:
Student.prototype.constructor;
Out[41]:
[Function: Person]

Reset the constructor property

In [42]:
Student.prototype.constructor = Student;
Out[42]:
[Function: Student]

Add a new method to Student (child)

In [43]:
Student.prototype.status = function(){
  return "I am currently a student"
}
Out[43]:
[Function]

Create a new Person object (parent)

In [44]:
var elie = new Person('Elie', 'Schoppik');

Cannot find method status in Person (parent)

elie.status; // undefined

How about using 'new'?

  • This will do almost the same thing, but add additional unnecessary properties on the prototype object

Exercise: Inheritance

1 - Create a constructor function for a Vehicle. Each vehicle should have a make, model and year property.

In [45]:
function Vehicle(make, model, year){
  this.make = make;
  this.model = model;
  this.year = year;
}

2 - Add a function to the Vehicle prototype called start which returns the string "VROOM!"

In [46]:
Vehicle.prototype.start = function(){
  return "VROOM!";
}
Out[46]:
[Function]

3 - Add a function to the Vehicle prototype called toString which returns the string "The make, model, and year are" concatenated with the make, model and year property

Examples

var vehicle = new Vehicle("Tractor", "John Deere", 1999)
 vehicle.toString() // 'The make, model, and year are Tractor John Deere 1999'
In [47]:
Vehicle.prototype.toString = function(){
  return "The make, model, and year are " + this.make + " " + this.model + " " + this.year;
}
Out[47]:
[Function]

4 - Create a constructor function for a Car. Each object created from the Car function should also have a make, model, and year and a property called numWheels which should be 4. The Car prototype should inherit all of the methods from the Vehicle prototype

In [49]:
function Car(){
  Vehicle.apply(this, arguments);
  this.numWheels = 4;
}

Car.prototype = Object.create(Vehicle.prototype);
Car.prototype.constructor = Car;
Out[49]:
[Function: Car]

5 - Create a constructor function for a Motorcycle. Each object created from the Motorcycle function should also have a make, model, and year and a property called numWheels which should be 2. The Motorcycle prototype should inherit all of the methods from the Vehicle prototype

In [50]:
function Motorcycle(){
  Vehicle.apply(this, arguments);
  this.numWheels = 2;
}

Motorcycle.prototype = Object.create(Vehicle.prototype);
Motorcycle.prototype.constructor = Motorcycle;
Out[50]:
[Function: Motorcycle]