🪴 yshalsager's Digital Garden

Search

Search IconIcon to open search

JavaScript OOP

Last updated Jul 7, 2022

المعرفة:: JavaScript
الحالة:: #ملاحظة_مؤرشفة
المراجع:: The Complete JavaScript Course 2022 From Zero to Expert, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain, https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes


# What is Object Oriented Programming (OOP)?

# 4 Fundamental OOP Principles

# 1. Abstraction

Ignoring or hiding details that dont matter, allowing us to get an overview perspective of the thing were implementing, instead of messing with details that dont really matter to our implementation.

# 2. Encapsulation

# 3. 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.

# 4. Polymorphism

A child class can overwrite a method it inherited from a parent class [its more complex that that, but enough for our purposes].

# Classical OOP vs Prototypes

Classical OOPPrototypes
Objects (instances) are instantiated from a class, which functions like a blueprint.Objects are linked to a prototype object
Behavior (methods) is copied from class to all instances.Prototypal inheritance: The prototype contains methods (behavior) that are accessible to all objects linked to that prototype.
Behavior is delegated to the linked prototype object.

# Prototypes Introduction

# 3 Ways Of Implementing Prototypal Inheritance

1. Constructor Functions

2. ES6 Classes

3. Object.create()

# Constructor Functions and new Operator

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const Person = function (firstName, birthYear) {  
  // Instance properties  
  this.firstName = firstName;  
  this.birthYear = birthYear;  
};  
// 1. New object is created  
// 2. function is called, this = the new object  
// 3. the new object linked to prototype  
// 4. function automatically return the new object  
const jonas = new Person('Jonas', 1991); // Person { firstName: 'Jonas', birthYear: 1991 }  
const matilda = new Person('Matilda', 2017); // Person { firstName: 'Matilda', birthYear: 2017 }  
const jack = new Person('Jack', 1975); // Person { firstName: 'Jack', birthYear: 1975 }  

# How does Constructor Functions work?

Here’s what happen by using new operator:

  1. An empty object is created.
  2. this keyword in constructor function call is set to the new object.
  3. The new object is linked (proto property) to the constructor function’s prototype property.
  4. The new object is returned from the constructor function call. (Unless we’re explicitly return something else)

# instanceof

1
console.log(jonas instanceof Person); // true  

# Methods in Constructor Functions

1
2
3
4
5
6
7
8
9
const Person = function (firstName, birthYear) {  
  // Instance properties  
  this.firstName = firstName;  
  this.birthYear = birthYear;  
  // Never to this!  
  this.calcAge = function () {  
    console.log(2037 - this.birthYear);  
  };  
};  

# Prototypes

1
2
3
4
5
6
7
8
// Prototypes  
console.log(Person.prototype); // it will have calcAge function  
Person.prototype.calcAge = function () { // With this only one copy of the function will exist  
  console.log(2037 - this.birthYear);  
};  
// It works because any object always has access to the methods and properties from its prototype.  
jonas.calcAge(); // 46  
matilda.calcAge(); // 20  

# __proto__

1
console.log(jonas.__proto__ === Person.prototype); // true  

# isPrototypeOf()

1
2
3
console.log(Person.prototype.isPrototypeOf(jonas)); // true  
console.log(Person.prototype.isPrototypeOf(matilda)); // true  
console.log(Person.prototype.isPrototypeOf(Person)); // false  

# Prototype Properties

1
2
Person.prototype.species = 'Homo Sapiens';  
console.log(jonas.species, matilda.species); // Homo Sapiens  

# hasOwnProperty()

1
2
console.log(jonas.hasOwnProperty('firstName')); // true  
console.log(jonas.hasOwnProperty('species')); // false  

# How Prototypal Inheritance / Delegation Works

# How Prototypal Inheritance works with function constructors and ES6 classes

graph BT const_person[Constructor function
Person] --.prototype--> person person[Prototype
Person.prototype
calcAge: function] --.constructor --> const_person jonas[Object
jonas
name: 'jonas'
birthYear: 1990
__proto__:
Person.prototype] --.__proto__--> person

# The Prototype Chain

Series of links between objects, linked through prototypes (Similar to the Scope Chain)

Explanation:

graph BT jonas[Object
jonas
__proto__:
Person.prototype] --.__proto__--> person[Prototype
Person.prototype
__proto__:
Object.prototype] person --.__proto__--> object[Prototype
Object.prototype
__proto__: null] object --.__proto__--> null[null] const_person[Constructor function
Person] --> person const_object[Constructor function
Object] --> object
1
2
3
console.log(jonas.__proto__); // Person.prototype  
console.log(jonas.__proto__.__proto__); // Object.prototype (top of prototype chain)  
console.log(jonas.__proto__.__proto__.__proto__); // null  

# Prototypal Inheritance on Built-In Objects

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

# Extending Built-In Objects Functionality

1
2
3
4
Array.prototype.unique = function () {  
  return [...new Set(this)];  
};  
console.log(arr.unique()); // [ 3, 6, 5, 9 ]  

# ES6 Classes

# Class Declarations

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class PersonCl {  
  constructor(fullName, birthYear) {  
    this.fullName = fullName; // Instance properties  
    this.birthYear = birthYear;  
  }  
}  
  
class Rectangle {  
  constructor(height, width) {  
    this.height = height;  
    this.width = width;  
  }  
}  
1
2
3
const p = new Rectangle(); // ReferenceError  
  
class Rectangle {}  

# Class Expressions

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// unnamed  
let Rectangle = class {  
  constructor(height, width) {  
    this.height = height;  
    this.width = width;  
  }  
};  
console.log(Rectangle.name); // Rectangle  
  
// named  
Rectangle = class Rectangle2 {  
  constructor(height, width) {  
    this.height = height;  
    this.width = width;  
  }  
};  
console.log(Rectangle.name); // Rectangle2  

# Methods Definition

# Constructor

# Prototype (Instance) Methods

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class PersonCl {  
  ...  
  // Instance methods  
  calcAge() {  
    console.log(2037 - this.birthYear);  
  }  
  greet() {  
    console.log(`Hey ${this.fullName}`);  
  }  
}  
  
const jessica = new PersonCl('Jessica Davis', 1996);  
jessica.calcAge(); // 41  
1
2
3
4
PersonCl.prototype.greet = function () {  
  console.log(`Hey ${this.firstName}`);  
};  
jessica.greet();  

# Getters and Setters

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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); // 300  
account.latest = 50;  
console.log(account.movements); // [ 200, 530, 120, 300, 50 ]  
1
2
3
4
5
6
7
8
9
class PersonCl {  
  ...  
  get age() {  
    return 2037 - this.birthYear;  
  }  
}  
  
const jessica = new PersonCl('Jessica Davis', 1996);  
console.log(jessica.age); // 41  
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class PersonCl {  
  constructor(fullName, birthYear) {  
    this.fullName = fullName;  
    this.birthYear = 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;  
  }  
}  

# Static Methods

1
2
Array.from(document.querySelectorAll('h1')); // [h1]  
[1, 2, 3].from() // TypeError  
1
2
3
4
5
6
Person.hey = function () {  
  console.log('Hey there!');  
  console.log(this); // constructor function itself  
};  
  
Person.hey();  
1
2
3
4
5
6
7
8
9
class PersonCl {  
  ...  
  // Static method  
  static hey() {  
    console.log('Hey there 👋');  
    console.log(this); // the entire class  
  }  
};  
PersonCl.hey();  

# Object.create()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
const PersonProto = {  
  calcAge() {  
    console.log(2037 - this.birthYear);  
  }  
};  
const steven = Object.create(PersonProto);  
console.log(steven); // {}  
steven.name = 'Steven';  
steven.birthYear = 2002;  
steven.calcAge(); // 35  
console.log(steven.__proto__ === PersonProto); // true  
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const PersonProto = {  
  calcAge() {  
    console.log(2037 - this.birthYear);  
  },  
  init(firstName, birthYear) {  
    this.firstName = firstName;  
    this.birthYear = birthYear;  
  },  
};  
const sarah = Object.create(PersonProto);  
sarah.init('Sarah', 1979);  
sarah.calcAge(); // 58  

# Inheritance

# Inheritance Between Classes: Constructor Functions

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// Parent  
const Person = function (firstName, birthYear) {  
  this.firstName = firstName;  
  this.birthYear = birthYear;  
};  
Person.prototype.calcAge = function () {  
  console.log(2037 - this.birthYear);  
};  
  
// Child  
// 1. Constructor Function  
const Student = function (firstName, birthYear, course) {  
  // this.firstName = firstName;  
  // this.birthYear = birthYear;  
  Person.call(this, firstName, birthYear); // set parent properties  
  this.course = course;  
};  
  
// 2. Linking prototypes  
Student.prototype = Object.create(Person.prototype);  
  
// Wrong way of doing it:  
// Student.prototype = Person.prototype; // This won't work as it sets Student.prototype to be the same object of Person.prototype  
  
// 3. Adding a new method the normal way  
Student.prototype.introduce = function () {  
  console.log(`My name is ${this.firstName} and I study ${this.course}`);  
};  
  
// Using child  
const mike = new Student('Mike', 2020, 'Computer Science');  
mike.introduce(); // My name is Mike and I study Computer Science  
mike.calcAge() // 17  
1
2
3
4
5
console.log(mike.__proto__); // Student  
console.log(mike.__proto__.__proto__); // Person  
console.log(mike instanceof Student);  
console.log(mike instanceof Person);  
console.log(mike instanceof Object);  
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
/**  
Student {firstName: 'Mike', birthYear: 2020, course: 'Computer Science'}  
	birthYear: 2020  
	course: "Computer Science"  
	firstName: "Mike"  
	[](Prototype): Person  
**/  
Student.prototype.constructor = Student;  
console.dir(Student.prototype.constructor);  
/**  
ƒ Student(firstName, birthYear, course)  
	arguments: null  
	caller: null  
	length: 3  
	name: "Student"  
	prototype: Person {introduce: ƒ, constructor: ƒ}  
	[](FunctionLocation): VM321:9  
	[](Prototype): ƒ ()  
	[](Scopes): Scopes[2]  
**/  
graph BT mike[Object
mike
__proto__:
Student.prototype] student[Prototype
Student.prototype
__proto__:
Person.prototype] person[Prototype
Person.prototype
__proto__:
Object.prototype] object[Prototype
Object.prototype
__proto__: null] mike --.__proto__--> student --.__proto__--> person --.__proto__--> object --.__proto__--> null[null] const_student[Constructor function
Student] --> student const_person[Constructor function
Person] --> person const_object[Constructor function
Object] --> object

# Inheritance Between Classes: ES6 Classes

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// Parent class  
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 👋');  
  }  
}  
  
// Child class  
class StudentCl extends PersonCl {  
  constructor(fullName, birthYear, course) {  
    // Always needs to happen first!  
    super(fullName, birthYear); // call the super class constructor and pass in fullName and birthYear parameters  
    this.course = course;  
  }  
  // New method  
  introduce() {  
    console.log(`My name is ${this.fullName} and I study ${this.course}`);  
  }  
  // Override parent method  
  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(); // My name is Martha Jones and I study Computer Science  
martha.calcAge(); // I'm 25 years old, but as a student I feel more like 35  

# Inheritance Between Classes: Object.create()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Parent  
const PersonProto = {  
  calcAge() {  
    console.log(2037 - this.birthYear);  
  },  
  init(firstName, birthYear) {  
    this.firstName = firstName;  
    this.birthYear = birthYear;  
  },  
};  
const steven = Object.create(PersonProto);  
// Child  
const StudentProto = Object.create(PersonProto); // 1. Create  
StudentProto.init = function (firstName, birthYear, course) { // 2. Init  
  PersonProto.init.call(this, firstName, birthYear); // set parent init parameters  
  this.course = course; // set rest child parameters  
};  
// A new method  
StudentProto.introduce = function () {  
  console.log(`My name is ${this.firstName} and I study ${this.course}`);  
};  
// Create and use a new child object  
const jay = Object.create(StudentProto);  
jay.init('Jay', 2010, 'Computer Science');  
jay.introduce(); // My name is Jay and I study Computer Science  
jay.calcAge(); // 27  
graph BT jay[Object
Jay
__proto__:
StudentProto] student[Prototype
StudentProto
__proto__:
PersonProto] person[Prototype
PersonProto
__proto__:
Object.prototype] object[Prototype
Object.prototype
__proto__: null] jay --.__proto__--> student --.__proto__--> person --.__proto__--> object --.__proto__--> null[null]

# Protected Properties and Methods

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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, ${owner}`);  
  }  
  // Public interface  
  getMovements() {  
    return this._movements;  
  }  
  deposit(val) {  
    this._movements.push(val);  
  }  
  withdraw(val) {  
    this.deposit(-val);  
  }  
  _approveLoad(val) {  
    return true;  
  }  
  requestLoan(val) {  
    if (this._approveloan(val)) {  
    this.deposit(val);  
    console.log('Loan approved');  
  }  
}  

# Class Fields and Methods

# Public field declarations

1
2
3
4
5
6
7
8
class Rectangle {  
  height = 0;  
  width;  
  constructor(height, width) {  
    this.height = height;  
    this.width = width;  
  }  
}  

# Public static fields

1
2
3
4
class ClassWithStaticField {  
  static staticField = 'static field'  
}  
console.log(ClassWithStaticField.staticField) // "static field"  

# Private Class Fields and Methods

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
class ClassWithPrivateMethod {  
  #privateMethod() {  
    return 'hello world';  
  }  
  
  getPrivateMessage() {  
    return this.#privateMethod();  
  }  
}  
  
const instance = new ClassWithPrivateMethod();  
console.log(instance.getPrivateMessage()); // hello world  

# Example

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// 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)) {  
      this.deposit(val);  
      console.log(`Loan approved`);  
      return this;  
    }  
  }  
  static helper() {  
    console.log('Helper');  
  }  
  // 4) Private methods  
  #approveLoan(val) {  
    return true;  
  }  
}  
const acc1 = new Account('Jonas', 'EUR', 1111); // "Thanks for opening an account, Jonas"  
// acc1._movements.push(250);  
// acc1._movements.push(-140);  
// acc1.approveLoan(1000);  
acc1.deposit(250);  
acc1.withdraw(140);  
acc1.requestLoan(1000); // "Loan approved"  
console.log(acc1.getMovements()); // [250, -140, 1000]  
console.log(acc1); // { currency: "EUR", locale: "en-US", owner: "Jonas" }  
Account.helper(); // "Helper"  
// console.log(acc1.#movements);  
// console.log(acc1.#pin);  
// console.log(acc1.#approveLoan(100));  

# Methods Chaining

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

# ES6 Classes Summary

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class Student extends Person {  
  /*   
  Person: Parent class.  
  extends: Inheritance between classes, automatically sets prototype.  
  Student: Child class.  
  */  
  university = "University of Lishon"; // Public field (similar to property, available on created object)  
  static numSubjects = 10; // Static public field (available only on class)  
  #studyHours = 0; // Private fields (not accessible outside of class)  
  #course; // undefined private field  
  constructor(fullName, birthYear, startYear, course) {  
    /*  
    Constructor method, called by new operator. Mandatory in regular class, might be omitted in a child class.  
    */  
    super(fullName, birthYear); // Call to parent (super) class (necessary with extend). Needs to happen before accessing this  
    this.startYear = startYear; // Instance property (available on created object)  
    this.#course = course; // Redefining private field  
  }  
  introduce() { // Public method  
    console.log(`I study $S${this.#course} at ${this.university}`);  
  }  
  study(h) {  
    this.#makeCoffe(); // Referencing private field and method  
    this.#studyHours += h;  
  }  
  #makeCoffe() { // Private method (⚠ Might not yet work in your browser. “Fake” alternative: _ instead of #)  
    return "Here is a coffe for you";  
  }  
  get testScore() { // Getter method  
    return this._testScore;  
  }  
  set testScore(score) { // Setter method (use _ to set property with same name as method, and also add getter)  
    this._testScore = score < 20 ? score : 0;  
  }  
  static printCurriculum() { // Static method (available only on class. Can not access instance properties nor methods, only static ones)  
    console.log(`There are ${this.numSubjects} subjects`);  
  }  
}  
// Creating new object with new operator  
const student = new Student("Jonas", 2020, 2037, "Medicine");