Skip to content

Latest commit

 

History

History
972 lines (741 loc) · 27 KB

File metadata and controls

972 lines (741 loc) · 27 KB

🏗️ 13. Object-Oriented Programming (O0P) With JavaScript

Topic: OOP Prototypes, constructor functions, ES6 classes, inheritance, and encapsulation.

📑 Table of Contents


What is Object Oriented Programming?

📝 OOP in Plain English: Object-Oriented Programming is a paradigm where you organize code into objects that contain both data (properties) and behavior (methods). Instead of writing a long list of instructions, you model your program as interacting objects — just like the real world.

The Four Pillars of OOP:

Pillar Real-World Analogy In Code
Abstraction Car dashboard hides engine complexity Hide internal details, expose simple interface
Encapsulation Bank vault keeps money secure Keep data private, control access via methods
Inheritance Child inherits parent's traits Child class reuses parent class code
Polymorphism "Open" works on doors, drawers, apps Same method name behaves differently per class

💡 Why OOP? It helps you organize large codebases, reduce duplication, and model real-world relationships in your software.

🖼️ Visual: Object Oriented Programming (O0P) With JavaScript

Classes and Instances (Traditional OOP)

🖼️ Visual: Object Oriented Programming (O0P) With JavaScript

The 4 Fundamental OOP Principle

🖼️ Visual: Object Oriented Programming (O0P) With JavaScript

Abstraction

  • Abstraction: Ignoring or hiding details that don't matter, allowing us to get an overview perspective of the thing we're implementing, instead of messing with details that don't really matter to our implementation.

🖼️ Visual: Object Oriented Programming (O0P) With JavaScript

Encapsulation Inheritance

  • Encapsulation: Keeping properties and methods private inside the class, SO they are not accessible from outside the class. Some methods can be exposed as a public interface (API).

🖼️ Visual: Object Oriented Programming (O0P) With JavaScript

Polymorphism

  • Inheritance: Making all properties and methods of a certain class available to a child class, forming a hierarchical relationship between classes. This allows us to reuse common logic and to model real-world relationships.

🖼️ Visual: Object Oriented Programming (O0P) With JavaScript


00P in JavaScript

  • Polymorphism: A child class can overwrite a method it inherited from a parent class [it's more complex that that, but enough for our purposes].

🖼️ Visual: Object Oriented Programming (O0P) With JavaScript


00P in JavaScript

🖼️ Visual: Object Oriented Programming (O0P) With JavaScript

3 Ways of Implementing Prototypical Inheritance

🖼️ Visual: Object Oriented Programming (O0P) With JavaScript


Constructor Functions and the new Operator

📝 The new Operator in 4 Steps:

  1. Create an empty object: {}
  2. Set this to point to that empty object
  3. Link the object's __proto__ to the constructor's prototype
  4. Return the object automatically (unless you explicitly return another object)

Why Constructor Functions? Before ES6 classes (2015), constructor functions were the standard way to create reusable object blueprints in JavaScript. They simulate classes using functions and prototypes.

⚠️ Important: Always use new with constructor functions. Calling without new sets this to the global object (or undefined in strict mode), polluting the global scope or failing silently.

  • A constructor function is identical to a regular function except for how it's called. Prefixing with new triggers four special steps that create and initialize an object.
  • An arrow function will actually not work as a function constructor. And that's because it doesn't have its own this keyword
  • And constructor functions have been used since the beginning of JavaScript to kind of simulate classes.
  • Just note that function constructors are not really a feature of the JavaScript language. Instead, they are simply a pattern that has been developed by other developers. And now everyone simply uses this.
/* all the objects that are created through this constructor function here will inherit, so they will get access to all the methods and properties that are defined on this prototype property. */
const Person = function (firstName, birthYear) {
  // Instance properties
  this.firstName = firstName;
  this.birthYear = birthYear;

  // You should never create a method inside of a constructor function
  // This.calcAge = 2047 - this.birthYear;
};

new Person('Ahmed', 1997);

// 1. New {} is created
// 2. function is called, this = {}
// 3. {} is linked to prototype
// 4. function automatically return {}

const ahmed = new Person('Ahmed', 1997);
const salaria = new Person('Salaria', 1998);

console.log(ahmed);
console.log(salaria);

console.log(ahmed instanceof Person); // true

Prototypes

📝 The Prototype Chain: Every JavaScript object has an internal link (__proto__) to another object called its prototype. When you access a property that doesn't exist on an object, JavaScript walks up the prototype chain looking for it.

Prototype vs. __proto__:

  • Constructor.prototype is the template for all instances
  • instance.__proto__ is the link to that template
  • Object.getPrototypeOf(instance) is the modern way to read the link

🎯 Key Insight: Person.prototype is NOT the prototype of Person. It IS the prototype of all objects created by new Person(). This naming confusion trips up almost every beginner.

// Prototypes
console.log(Person.prototype); // → {constructor: ƒ, calcAge: ƒ}

// Add a method to the prototype — all instances inherit it
Person.prototype.calcAge = function () {
  console.log(2047 - this.birthYear);
};

ahmed.calcAge();  // → 50 (2047 - 1997)
salaria.calcAge(); // → 49 (2047 - 1998)

console.log(ahmed.__proto__); // → Person.prototype

// Where does __proto__ come from? Step 3 of the 'new' process:
// 1. New {} is created
// 2. function is called, this = {}
// 3. {} is linked to prototype (__proto__ = Constructor.prototype)
// 4. function automatically returns {}

console.log(ahmed.__proto__ === Person.prototype); // → true

/* So person dot prototype here is actually not the prototype of person. But instead, it is what's gonna be used as the prototype of all the objects that are created with the person constructor function.
So that's a subtle a but important difference that you need to keep in mind. And, in fact, what I just said that is confirmed by this comparison */

console.log(Person.prototype.isPrototypeOf(ahmed)); // true
console.log(Person.prototype.isPrototypeOf(salaria)); // true
console.log(Person.prototype.isPrototypeOf(Person)); // false

// Rather than calling it prototype is should have been called .prototypeOfLinkedObjects

Person.prototype.species = 'Homo Sapiens';
console.log(ahmed.species, salaria.species);

console.log(ahmed.hasOwnPropery('firstName')); // true
console.log(ahmed.hasOwnPropery('species')); // false

Prototypal Inheritance and The Prototype Chain

How Prototypical Inheritance / Delegation works?

🖼️ Visual: Object Oriented Programming (O0P) With JavaScript The Prototype Chain 🖼️ Visual: Object Oriented Programming (O0P) With JavaScript


Prototypal Inheritance on Built-In Objects

///////////////////////////////////////
// Prototypal Inheritance on Built-In Objects
console.log(jonas.__proto__); // → Person.prototype
// Object.prototype (top of prototype chain)
console.log(jonas.__proto__.__proto__); // → Object.prototype (top of chain)
console.log(jonas.__proto__.__proto__.__proto__); // → null (chain ends)

console.dir(Person.prototype.constructor);

const arr = [3, 6, 6, 5, 6, 9, 9]; // new Array === []
console.log(arr.__proto__);
console.log(arr.__proto__ === Array.prototype); // → true

console.log(arr.__proto__.__proto__);

Array.prototype.unique = function () {
  return [...new Set(this)];
};

console.log(arr.unique()); // → [3, 6, 5, 9]

const h1 = document.querySelector('h1');
console.dir(x => x + 1);

Coding Challenge #1

  1. Use a constructor function to implement a Car. A car has a make and a speed property. The speed property is the current speed of the car in km/h;
  2. Implement an 'accelerate' method that will increase the car's speed by 10, and log the new speed to the console;
  3. Implement a 'brake' method that will decrease the car's speed by 5, and log the new speed to the console;
  4. Create 2 car objects and experiment with calling 'accelerate' and 'brake' multiple times on each of them.

DATA CAR 1: 'BMW' going at 120 km/h DATA CAR 2: 'Mercedes' going at 95 km/h

const Car = function (make, speed) {
  this.make = make;
  this.speed = speed;
};

Car.prototype.accelerate = function () {
  this.speed += 10;
  console.log(`${this.make} is going at ${this.speed} km/h`);
};

Car.prototype.brake = function () {
  this.speed -= 5;
  console.log(`${this.make} is going at ${this.speed} km/h`);
};

const bmw = new Car('BMW', 120);
const mercedes = new Car('Mercedes', 95);

bmw.accelerate();
bmw.accelerate();
bmw.brake();
bmw.accelerate();

ES6 Classes

///////////////////////////////////////
// ES6 Classes

// Class expression
// Const PersonCl = class {}

// Class declaration
class PersonCl {
  constructor(fullName, birthYear) {
    this.fullName = fullName;
    this.birthYear = birthYear;
  }

  // Instance methods
  // Methods will be added to .prototype property
  calcAge() {
    console.log(2037 - this.birthYear);
  }

  greet() {
    console.log(`Hey ${this.fullName}`);
  }

  get age() {
    return 2037 - this.birthYear;
  }

  // Set a property that already exists
  set fullName(name) {
    if (name.includes(' ')) this._fullName = name;
    else alert(`${name} is not a full name!`);
  }

  get fullName() {
    return this._fullName;
  }

Setters and Getters

  • So every object in JavaScript can have setter and getter properties. And we call these special properties assessor properties, while the more normal properties are called data properties. So getters and setters are basically functions that get and set a value so just as the name says, but on the outside they still look like regular properties.
///////////////////////////////////////
// Setters and Getters
const account = {
  owner: 'Jonas',
  movements: [200, 530, 120, 300],

  get latest() {
    return this.movements.slice(-1).pop();
  },

  set latest(mov) {
    this.movements.push(mov);
  },
};

console.log(account.latest);

account.latest = 50;
console.log(account.movements);

Static Methods

  // Static method
  static hey() {
    console.log('Hey there 👋');
    console.log(this);
  }
}

const jessica = new PersonCl('Jessica Davis', 1996);
console.log(jessica);
jessica.calcAge();
console.log(jessica.age);

console.log(jessica.__proto__ === PersonCl.prototype);

// PersonCl.prototype.greet = function () {
//   console.log(`Hey ${this.firstName}`);
// };
jessica.greet();

// 1. Classes are NOT hoisted
// 2. Classes are first-class citizens
// 3. Classes are executed in strict mode

const walter = new PersonCl('Walter White', 1965);
// PersonCl.hey();

Object.create

///////////////////////////////////////
// Object.create
const PersonProto = {
  calcAge() {
    console.log(2037 - this.birthYear);
  },

  init(firstName, birthYear) {
    this.firstName = firstName;
    this.birthYear = birthYear;
  },
};

const steven = Object.create(PersonProto);
console.log(steven);
steven.name = 'Steven';
steven.birthYear = 2002;
steven.calcAge();

console.log(steven.__proto__ === PersonProto);

const sarah = Object.create(PersonProto);
sarah.init('Sarah', 1979);
sarah.calcAge();

Coding Challenge #2

class CarCl {
  constructor(make, speed) {
    this.make = make;
    this.speed = speed;
  }

  accelerate() {
    this.speed += 10;
    console.log(`${this.make} is going at ${this.speed} km/h`);
  }

  brake() {
    this.speed -= 5;
    console.log(`${this.make} is going at ${this.speed} km/h`);
  }

  get speedUS() {
    return this.speed / 1.6;
  }

  set speedUS(speed) {
    this.speed = speed * 1.6;
  }
}

const ford = new CarCl('Ford', 120);
console.log(ford.speedUS);
ford.accelerate();
ford.accelerate();
ford.brake();
ford.speedUS = 50;
console.log(ford);

Inheritance Between "Classes": Constructor Functions

///////////////////////////////////////
// Inheritance Between "Classes": Constructor Functions

const Person = function (firstName, birthYear) {
  this.firstName = firstName;
  this.birthYear = birthYear;
};

Person.prototype.calcAge = function () {
  console.log(2037 - this.birthYear);
};

const Student = function (firstName, birthYear, course) {
  Person.call(this, firstName, birthYear);
  this.course = course;
};

// Linking prototypes
Student.prototype = Object.create(Person.prototype); // At this point, student.prototype is empty

Student.prototype.introduce = function () {
  console.log(`My name is ${this.firstName} and I study ${this.course}`);
};

const mike = new Student('Mike', 2020, 'Computer Science');
mike.introduce();
mike.calcAge();

console.log(mike.__proto__);
console.log(mike.__proto__.__proto__);

console.log(mike instanceof Student);
console.log(mike instanceof Person);
console.log(mike instanceof Object);

Student.prototype.constructor = Student;
console.dir(Student.prototype.constructor);

Coding Challenge #3

  1. Use a constructor function to implement an Electric Car (called EV) as a CHILD "class" of Car. Besides a make and current speed, the EV also has the current battery charge in % ('charge' property);
  2. Implement a 'chargeBattery' method which takes an argument 'chargeTo' and sets the battery charge to 'chargeTo';
  3. Implement an 'accelerate' method that will increase the car's speed by 20, and decrease the charge by 1%. Then log a message like this: 'Tesla going at 140 km/h, with a charge of 22%';
  4. Create an electric car object and experiment with calling 'accelerate', 'brake' and 'chargeBattery' (charge to 90%). Notice what happens when you 'accelerate'! HINT: Review the definiton of polymorphism 😉

DATA CAR 1: 'Tesla' going at 120 km/h, with a charge of 23%

const Car = function (make, speed) {
  this.make = make;
  this.speed = speed;
};

Car.prototype.accelerate = function () {
  this.speed += 10;
  console.log(`${this.make} is going at ${this.speed} km/h`);
};

Car.prototype.brake = function () {
  this.speed -= 5;
  console.log(`${this.make} is going at ${this.speed} km/h`);
};

const EV = function (make, speed, charge) {
  Car.call(this, make, speed);
  this.charge = charge;
};

// Link the prototypes
EV.prototype = Object.create(Car.prototype);

EV.prototype.chargeBattery = function (chargeTo) {
  this.charge = chargeTo;
};

EV.prototype.accelerate = function () {
  this.speed += 20;
  this.charge--;
  console.log(
    `${this.make} is going at ${this.speed} km/h, with a charge of ${this.charge}`
  );
};

const tesla = new EV('Tesla', 120, 23);
tesla.chargeBattery(90);
console.log(tesla);
tesla.brake();
tesla.accelerate();

Inheritance Between "Classes": ES6 Classes

🖼️ Visual: Object Oriented Programming (O0P) With JavaScript

🖼️ Visual: Object Oriented Programming (O0P) With JavaScript

🖼️ Visual: Object Oriented Programming (O0P) With JavaScript

🖼️ Visual: Object Oriented Programming (O0P) With JavaScript

///////////////////////////////////////
// Inheritance Between "Classes": ES6 Classes

class PersonCl {
  constructor(fullName, birthYear) {
    this.fullName = fullName;
    this.birthYear = birthYear;
  }

  // Instance methods
  calcAge() {
    console.log(2037 - this.birthYear);
  }

  greet() {
    console.log(`Hey ${this.fullName}`);
  }

  get age() {
    return 2037 - this.birthYear;
  }

  set fullName(name) {
    if (name.includes(' ')) this._fullName = name;
    else alert(`${name} is not a full name!`);
  }

  get fullName() {
    return this._fullName;
  }

  // Static method
  static hey() {
    console.log('Hey there 👋');
  }
}

class StudentCl extends PersonCl {
  constructor(fullName, birthYear, course) {
    // Always needs to happen first!
    super(fullName, birthYear);
    this.course = course;
  }

  introduce() {
    console.log(`My name is ${this.fullName} and I study ${this.course}`);
  }

  calcAge() {
    console.log(
      `I'm ${
        2037 - this.birthYear
      } years old, but as a student I feel more like ${
        2037 - this.birthYear + 10
      }`
    );
  }
}

const martha = new StudentCl('Martha Jones', 2012, 'Computer Science');
martha.introduce();
martha.calcAge();

Inheritance Between "Classes": Object.create

🖼️ Visual: Object Oriented Programming (O0P) With JavaScript

///////////////////////////////////////
// Inheritance Between "Classes": Object.create

const PersonProto = {
  calcAge() {
    console.log(2037 - this.birthYear);
  },

  init(firstName, birthYear) {
    this.firstName = firstName;
    this.birthYear = birthYear;
  },
};

const steven = Object.create(PersonProto);

const StudentProto = Object.create(PersonProto);
StudentProto.init = function (firstName, birthYear, course) {
  PersonProto.init.call(this, firstName, birthYear);
  this.course = course;
};

StudentProto.introduce = function () {
  // BUG in video:
  // console.log(`My name is ${this.fullName} and I study ${this.course}`);

  // FIX:
  console.log(`My name is ${this.firstName} and I study ${this.course}`);
};

const jay = Object.create(StudentProto);
jay.init('Jay', 2010, 'Computer Science');
jay.introduce();
jay.calcAge();

Another Class Example

class Account {
  constructor(owner, currency, pin) {
    this.owner = owner;
    this.currency = currency;
    this.pin = pin;
    this.movements = [];
    this.locale = navigator.language;
    console.log(`Thanks for opening an account ${this.name}`);
  }

  // Public Interface

  deposit(val) {
    this.movements.push(val);
  }
  withdraw(val) {
    this.deposit(-val);
  }
  approveLoan(val) {
    return true;
  }
  requestLoan(val) {
    if (this.approveLoan(val)) {
      this.deposit(val);
      console.log('Loan Approved');
    }
  }
}

const acc1 = new Account('xoraus', 'RUP', 1111);

acc1.deposit(250);
acc1.deposit(140);
acc1.requestLoan(1000);
// Acc1.approveLoan(1000); This method should only be available to the requestLoan method
// Therefore we need data encapsulation and data privacy

console.log(acc1);
// console.log(acc1.pin); - This should not be accessible

Encapsulation: Protected Properties and Methods

📝 Data Privacy in JavaScript: True encapsulation (private data) has historically been difficult in JavaScript. The language has evolved through three privacy conventions:

Approach Syntax Truly Private?
Convention _property (underscore) ❌ No — just a hint
Closures Variables in constructor scope ✅ Yes — but per-instance
ES2022 Private Fields #property ✅ Yes — language-level

💡 Best Practice: Use ES2022 private fields (#balance) in modern projects. For older codebases or when you need shared private methods, use closures or WeakMaps.

  • Now first, remember that encapsulation basically means to keep some properties and methods private inside the class so that they are not accessible from outside of the class. Then the rest of the methods are basically exposed as a public interface, which we can also call API.
  • Now, there are two big reasons why we need encapsulation and data privacy.
  • So first it is to prevent code from outside of a class to accidentally manipulate or data inside the class.
  • Now, the second reason is that when we expose only a small interface so a small API consisting only of a few public methods then we can change all the other internal methods with more confidence. Because in this case, we can be sure that external code does not rely on these private methods.
  • Encapsulation means information hiding. It's about hiding as much as possible of the object's internal parts and exposing a minimal public interface. The simplest and most elegant way to create encapsulation in JavaScript is using closures. A closure can be created as a function with private state.
'use strict';

class Account {
  constructor(owner, currency, pin) {
    this.owner = owner;
    this.currency = currency;

    // Protected Property
    this._pin = pin;
    this._movements = [];
    this.locale = navigator.language;
    console.log(`Thanks for opening an account ${this.name}`);
  }

  // Public Interface

  getMovements() {
    return this._movements;
  }

  deposit(val) {
    this._movements.push(val);
  }
  withdraw(val) {
    this.deposit(-val);
  }
  _approveLoan(val) {
    return true;
  }
  requestLoan(val) {
    if (this._approveLoan(val)) {
      this.deposit(val);
      console.log('Loan Approved');
    }
  }
}

const acc1 = new Account('xoraus', 'RUP', 1111);

// Acc1._movements.push(250);
// Acc1._movements.push(-140);

acc1.deposit(250);
acc1.deposit(140);

acc1.requestLoan(1000);
// Acc1.approveLoan(1000); This method should only be available to the requestLoan method
// Therefore we need data encapsulation and data privacy

console.log(acc1.getMovements());

console.log(acc1);
// console.log(acc1.pin); - This should not be accessible

Encapsulation: Private Class Fields and Methods

///////////////////////////////////////
// Encapsulation: Protected Properties and Methods
// Encapsulation: Private Class Fields and Methods

// 1) Public fields
// 2) Private fields
// 3) Public methods
// 4) Private methods
// (there is also the static version)

class Account {
  // 1) Public fields (instances)
  locale = navigator.language;

  // 2) Private fields (instances)
  #movements = [];
  #pin;

  constructor(owner, currency, pin) {
    this.owner = owner;
    this.currency = currency;
    this.#pin = pin;

    // Protected property
    // This._movements = [];
    // This.locale = navigator.language;

    console.log(`Thanks for opening an account, ${owner}`);
  }

  // 3) Public methods

  // Public interface
  getMovements() {
    return this.#movements;
  }

  deposit(val) {
    this.#movements.push(val);
    return this;
  }

  withdraw(val) {
    this.deposit(-val);
    return this;
  }

  requestLoan(val) {
    // If (this.#approveLoan(val)) {
    if (this._approveLoan(val)) {
      this.deposit(val);
      console.log(`Loan approved`);
      return this;
    }
  }

  static helper() {
    console.log('Helper');
  }

  // 4) Private methods
  // #approveLoan(val) {
  _approveLoan(val) {
    return true;
  }
}

const acc1 = new Account('Jonas', 'EUR', 1111);

// Acc1._movements.push(250);
// Acc1._movements.push(-140);
// Acc1.approveLoan(1000);

acc1.deposit(250);
acc1.withdraw(140);
acc1.requestLoan(1000);
console.log(acc1.getMovements());
console.log(acc1);
Account.helper();

// console.log(acc1.#movements);
// console.log(acc1.#pin);
// console.log(acc1.#approveLoan(100));

Chaining Methods

// Chaining
acc1.deposit(300).deposit(500).withdraw(35).requestLoan(25000).withdraw(4000);
console.log(acc1.getMovements());

ES6 Classes Summary

🖼️ Visual: Object Oriented Programming (O0P) With JavaScript


Coding Challenge #4

  1. Re-create challenge #3, but this time using ES6 classes: create an 'EVCl' child class of the 'CarCl' class
  2. Make the 'charge' property private;
  3. Implement the ability to chain the 'accelerate' and 'chargeBattery' methods of this class, and also update the 'brake' method in the 'CarCl' class. They experiment with chining!

DATA CAR 1: 'Rivian' going at 120 km/h, with a charge of 23%

class CarCl {
  constructor(make, speed) {
    this.make = make;
    this.speed = speed;
  }

  accelerate() {
    this.speed += 10;
    console.log(`${this.make} is going at ${this.speed} km/h`);
  }

  brake() {
    this.speed -= 5;
    console.log(`${this.make} is going at ${this.speed} km/h`);
    return this;
  }

  get speedUS() {
    return this.speed / 1.6;
  }

  set speedUS(speed) {
    this.speed = speed * 1.6;
  }
}

class EVCl extends CarCl {
  #charge;

  constructor(make, speed, charge) {
    super(make, speed);
    this.#charge = charge;
  }

  chargeBattery(chargeTo) {
    this.#charge = chargeTo;
    return this;
  }

  accelerate() {
    this.speed += 20;
    this.#charge--;
    console.log(
      `${this.make} is going at ${this.speed} km/h, with a charge of ${
        this.#charge
      }`
    );
    return this;
  }
}

const rivian = new EVCl('Rivian', 120, 23);
console.log(rivian);
// console.log(rivian.#charge);
rivian
  .accelerate()
  .accelerate()
  .accelerate()
  .brake()
  .chargeBattery(50)
  .accelerate();

console.log(rivian.speedUS);

📋 Cheat Sheet

Concept Rule / Syntax
Constructor function const Person = function(name){ this.name = name; }
Prototype method Person.prototype.greet = function(){}
ES6 Class class Person { constructor(name){} greet(){} }
Inheritance class Student extends Person
super() Call parent constructor
Private field #balance = 0
Static method static helper(){} — called on class, not instance

🎯 Key Insight: JavaScript uses prototypal inheritance under the hood. class is syntactic sugar over constructor functions and prototype.

🧭 Navigation

← 📄 12. Advanced DOM and Events | 🏠 Home | 📄 14. Mapty App - OOP, Geolocation, External Libraries, and More! →


Happy Coding! 🚀