![[13. Object-Oriented Programming (O0P) With JavaScript-1663898980491.jpeg]]
![[13. Object-Oriented Programming (O0P) With JavaScript-1663899008964.jpeg]]
![[13. Object-Oriented Programming (O0P) With JavaScript-1663899032649.jpeg]]
- 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. ![[13. Object-Oriented Programming (O0P) With JavaScript-1663899059332.jpeg]]
- 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). ![[13. Object-Oriented Programming (O0P) With JavaScript-1663899150174.jpeg]]
- 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. ![[13. Object-Oriented Programming (O0P) With JavaScript-1663899193652.jpeg]]
- 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]. ![[13. Object-Oriented Programming (O0P) With JavaScript-1663899232684.jpeg]]
![[13. Object-Oriented Programming (O0P) With JavaScript-1663899253131.jpeg]]
3 Ways of Implementing Prototypical Inheritance ![[13. Object-Oriented Programming (O0P) With JavaScript-1663899280934.jpeg]]
- The only difference between a regular function, and a function that we call constructor function, is that we call a constructor function with the new operator.
- 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 fucnction
// this.calcAge = 2047 - this.birthYear;
};
new Person('Ahmed', 1997);
// 1. New {} is created
// 2. function is called, this = {}
// 3. {} is linked to prototype
// 4. fucntion 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
console.log(Person.prototype);
Person.prototype.calcAge = function () {
console.log(2047 - this.birthYear);
};
ahmed.calcAge();
salaria.calcAge();
console.log(ahmed.__proto__);
// Where does this proto property comes from? From step number 3
// 1. New {} is created
// 2. function is called, this = {}
// 3. {} is linked to prototype
// 4. fucntion automatically return {}
console.log(ahmed.__proto__ === Person.prototype);
/* 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
How Prototypical Inheritance / Delegation works? ![[13. Object-Oriented Programming (O0P) With JavaScript-1663906658628.jpeg]] The Prototype Chain ![[13. Object-Oriented Programming (O0P) With JavaScript-1663906701353.jpeg]]
///////////////////////////////////////
// Prototypal Inheritance on Built-In Objects
console.log(jonas.__proto__);
// Object.prototype (top of prototype chain)
console.log(jonas.__proto__.__proto__);
console.log(jonas.__proto__.__proto__.__proto__);
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);
console.log(arr.__proto__.__proto__);
Array.prototype.unique = function () {
return [...new Set(this)];
};
console.log(arr.unique());
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();
![[13. Object-Oriented Programming (O0P) With JavaScript-1663994719743.jpeg]]
![[13. Object-Oriented Programming (O0P) With JavaScript-1663994740330.jpeg]]
![[13. Object-Oriented Programming (O0P) With JavaScript-1663994769380.jpeg]]
![[13. Object-Oriented Programming (O0P) With JavaScript-1663994814589.jpeg]]
///////////////////////////////////////
// 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();
![[13. Object-Oriented Programming (O0P) With JavaScript-1663994609934.jpeg]]
///////////////////////////////////////
// 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
- 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());
![[13. Object-Oriented Programming (O0P) With JavaScript-1663995283090.jpeg]]
- 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);