Topic:
OOPPrototypes, constructor functions, ES6 classes, inheritance, and encapsulation.
- What is Object Oriented Programming?
- 00P in JavaScript
- 00P in JavaScript
- Constructor Functions and the new Operator
- Prototypes
- Prototypal Inheritance and The Prototype Chain
- Prototypal Inheritance on Built-In Objects
- Coding Challenge #1
- ES6 Classes
- Setters and Getters
- Static Methods
- Object.create
- Coding Challenge #2
- Inheritance Between "Classes": Constructor Functions
- Coding Challenge #3
- Inheritance Between "Classes": ES6 Classes
- Inheritance Between "Classes": Object.create
- Another Class Example
- Encapsulation: Protected Properties and Methods
- Encapsulation: Private Class Fields and Methods
- Chaining Methods
- ES6 Classes Summary
- Coding Challenge #4
📝 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
🖼️ Visual: Object Oriented Programming (O0P) With JavaScript
🖼️ Visual: Object Oriented Programming (O0P) With JavaScript
- 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: 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
- 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
- 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
🖼️ Visual: Object Oriented Programming (O0P) With JavaScript
3 Ways of Implementing Prototypical Inheritance
🖼️ Visual: Object Oriented Programming (O0P) With JavaScript
📝 The
newOperator in 4 Steps:
- Create an empty object:
{} - Set
thisto point to that empty object - Link the object's
__proto__to the constructor'sprototype - 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 usenewwith constructor functions. Calling withoutnewsetsthisto the global object (orundefinedin 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
newtriggers 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📝 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.prototypeis the template for all instancesinstance.__proto__is the link to that templateObject.getPrototypeOf(instance)is the modern way to read the link
🎯 Key Insight:
Person.prototypeis NOT the prototype ofPerson. It IS the prototype of all objects created bynew 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')); // falseHow 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
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);- 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;
- Implement an 'accelerate' method that will increase the car's speed by 10, and log the new speed to the console;
- Implement a 'brake' method that will decrease the car's speed by 5, and log the new speed to the console;
- 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
// 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;
}- 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 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
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();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
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);- 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);
- Implement a 'chargeBattery' method which takes an argument 'chargeTo' and sets the battery charge to 'chargeTo';
- 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%';
- 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();🖼️ 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();🖼️ 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();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📝 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: 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
acc1.deposit(300).deposit(500).withdraw(35).requestLoan(25000).withdraw(4000);
console.log(acc1.getMovements());🖼️ Visual: Object Oriented Programming (O0P) With JavaScript
- Re-create challenge #3, but this time using ES6 classes: create an 'EVCl' child class of the 'CarCl' class
- Make the 'charge' property private;
- 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);| 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.
classis syntactic sugar over constructor functions andprototype.
← 📄 12. Advanced DOM and Events | 🏠 Home | 📄 14. Mapty App - OOP, Geolocation, External Libraries, and More! →
Happy Coding! 🚀