Thừa kế và đa hình trong ECMAScript

Xem thêm các chuyên mục:

1- Thừa kế trong Python

Trước khi bắt đầu học về "Thừa kế trong ECMAScript", hãy đảm bảo rằng bạn đã có khái niệm về "Lớp và đối tượng", nếu không hãy tìm hiểu nó:
ECMAScript cho phép bạn tạo một lớp mở rộng từ một hoặc nhiều lớp khác. Lớp này được gọi là lớp dẫn xuất (Derived class), hoặc đơn giản gọi là lớp con.
Lớp con sẽ thừa kế các property, phương thức và các thành viên khác từ lớp cha. Nó cũng có thể ghi đè (override) các phương thức từ lớp cha. Trong ECMAScript mỗi lớp chỉ có duy nhất 1 constructor. Nếu một lớp không tự định nghĩa một constructor của riêng nó, nó sẽ được thừa kế constructor của lớp cha. Ngược lại nếu lớp con định nghĩa một constructor, nó sẽ không được thừa kế constructor của lớp cha.
Khác với Java, CSharp và một vài ngôn ngữ khác, ECMAScript cho phép đa thừa kế. Một lớp có thể được mở rộng ( extends) từ một hoặc nhiều lớp cha.
Chúng ta cần một vài class tham gia vào minh họa.
  • Animal: Class mô phỏng một lớp Động vật.
  • Duck: Class mô phỏng lớp vịt, là một class con của Animal.
  • Cat: Class mô phỏng lớp mèo, là một class con của Animal
  • Mouse: Class mô phỏng lớp chuột, là một class con của Animal.
Trong ECMAScript, constructor sử dụng để tạo một đối tượng, và gán giá trị cho các property. Constructor của lớp con bao giờ cũng gọi tới constructor của lớp cha để gán giá trị cho các property của lớp cha, sau đó nó mới gán giá trị cho các property của nó.
Hãy xem ví dụ:
Cat là lớp con thừa kế từ lớp Animal, nó cũng có các property riêng của nó.
inherit-example1.js
class Animal {

   constructor(name)  {
     this.name = name;
   }

   showInfo()  {
     console.log("I'm " + this.name);
   }

   move()  {
     console.log("Moving..");
   }

}

class Cat extends Animal {
    constructor(name, age, height) {
       super(name);
       // Cat's properties:
       this.age = age;
       this.height = height;
    }

    // Ghi đè (override) phương thức cùng tên của lớp cha.
    showInfo()  {
      console.log("My name is " + this.name);
    }

    // Other method...
    sleep()  {
       console.log("Sleeping..");
    }
}

// ------------- TEST --------------------
let tom = new Cat("Tom", 3, 20);

console.log("Call move() method");

tom.move();

console.log("\n");
console.log("Call showInfo() method");

tom.showInfo();
Điều gì đã xẩy ra khi bạn khởi tạo một đối tượng từ constructor?. Nó sẽ gọi lên constructor của lớp cha như thế nào? Bạn hãy xem hình minh họa dưới đây:
Với hình minh họa trên bạn thấy rằng, constructor của lớp cha được gọi trong constructor của lớp con, nó sẽ gán giá trị cho các property của lớp cha, sau đó các property của lớp con cũng được gán giá trị.

2- Ghi đè phương thức

Mặc định lớp con được thừa kế các phương thức từ lớp cha, tuy nhiên lớp con có thể ghi đè (override) phương thức của lớp cha.
inheritance-example2.js
class Animal {

   constructor(name)  {
     this.name = name;
   }

   showInfo()  {
     console.log("I'm " + this.name);
   }

   move()  {
     console.log("Moving..");
   }

}

class Mouse extends Animal {

    constructor(name, age, height) {
       super(name);

       this.age = age;
       this.height = height;
    }

    // Ghi đè (override) phương thức cùng tên của lớp cha.
    showInfo()  {
      // Gọi phương thức showInfo() của lớp cha.
      super.showInfo();
      console.log ("Age: " + this.age);
      console.log ("Height: " + this.height);
    }

    // Ghi đè (override) phương thức cùng tên của lớp cha.
    move()  {
      console.log("Mouse Moving..");
    }
}

// ------------- TEST --------------------
let jerry = new Mouse("Jerry", 3, 5);

console.log("Call move() method");

jerry.move();

console.log("\n");
console.log("Call showInfo() method");

jerry.showInfo();

3- Phương thức trừu tượng

Khái niệm về một phương thức trừu tượng (abstract method) hoặc một lớp trừu tượng.(abstract class) có trong các ngôn ngữ như Java, C#. Nhưng nó không có một cách rõ ràng trong ECMAScript. Tuy nhiên chúng ta có cách để định nghĩa nó.
Một lớp được gọi là trừu tượng (abstract) định nghĩa ra các phương thức trừu tượng và lớp con phải ghi đè (override) các phương thức này nếu muốn sử dụng chúng. Các phương thức trừu tượng luôn ném ra ngoại lệ "Not Implemented Error".
abstract-example.js
// Một lớp trừu tượng (Abstract class).
class AbstractDocument {

    constructor(name) {
        this.name = name
    }

    // Một phương thức không thể sử dụng được, vì nó luôn ném ra lỗi.
    show() {
        // Not Implemented Error
        throw "Subclass must implement abstract method";
    }
}

class PDF extends AbstractDocument {

    // Ghi đè phương thức của lớp cha
    show() {
        console.log("Show PDF document:", this.name);
    }
}



class Word extends AbstractDocument {
    show() {
        console.log("Show Word document:", this.name)
    }
}



// -------------------- TEST -------------------------
let doc1 = new PDF("Python tutorial");
let doc2 = new Word("Java IO Tutorial");
let doc3 = new PDF("ECMAScript Date & Time Tutorial");

let documents = [doc1, doc2, doc3];


for (let i = 0; i < documents.length; i++) {
    documents[i].show();
}

let doc4 = new AbstractDocument("An Abstract Document");

doc4.show(); // Throw Error!!!
Ví dụ ở trên thể hiện tính đa hình (Polymorphism) trong ECMAScript. Một đối tượng Document (tài liệu) có thể được thể hiện ở các hình thái khác nhau ( PDF, Word, Excel, ...).
Một ví dụ khác minh họa về tính đa hình: Khi tôi nói về một người Châu Á, nó khá trừu tượng, anh ta có thể là một người Nhật Bản, người Việt Nam, hoặc một người Ấn Độ, ... Tuy nhiên đều có đặc điểm đặc trưng của người Châu Á.

4- Toán tử instanceof, typeof

instanceof

Toán tử instanceof giúp bạn kiểm tra xem một "cái gì đó" có phải là một đối tượng của một lớp nào đó hay không.
instanceof-example.js
class Person {

}

// A Child class of Person
class AsianPerson extends Person {

}

class Triangle {

}

let person = new Person();
let asianPerson = new AsianPerson();

let triangle = new Triangle();

let isPerson = person instanceof Person;
console.log( isPerson ); // true

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

console.log( person instanceof AsianPerson ); // false
console.log( triangle instanceof Person ); // false
console.log( triangle instanceof AsianPerson ); // false

typeof

Toán tử typeof được sử dụng để kiểm tra kiểu của một "cái gì đó", kết quả nhận được là tên của kiểu.
Type Result
undefined "undefined"
null "object"
boolean "boolean"
number "number"
String "string"
Symbol (new in ECMAScript 6) "symbol"
Host object (provided by the JS environment) Implementation-dependent
Function object (implements [[Call]] in ECMA-262 terms) "function"
Any other object "object"
typeof-example.js
// A Class:
class Person {

}
// An Object:
let person = new Person();

console.log( typeof Person ); // function
console.log( typeof person ); // object

// null:
console.log( typeof null ); // object

// A Number:
let intValue = 100;

console.log( typeof intValue ); // number

// NaN (Not a Number)
console.log( typeof NaN); // number

// A String
let strValue = "Hello";

console.log( typeof strValue ); // string

// A Boolean:
let boolValue = true;

console.log( typeof boolValue ); // boolean

// undefined
console.log( typeof undefined); // undefined

// An Array
let years = [1980, 2003, 2018];

console.log( typeof years ); // object

// A Function
let myFunc = function()  {

}

console.log( typeof myFunc ); // function

// A Symbol
let mySymbol =   Symbol();

console.log( typeof mySymbol ); // symbol

Is Sub Class?

Giả sử bạn có 2 lớp A & B. Ví dụ dưới đây giúp bạn kiểm tra xem A có phải là lớp con của B hay không.
let isChild = A === B || A.prototype instanceof B;
issubclass-example.js
class Animal {

}

class Cat extends Animal {
 
}

class AsianCat extends Cat {

}

// ------------- TEST --------------------

console.log("AsianCat === Animal? " + (AsianCat === Animal)); // false

let isSubClass1 = AsianCat === Animal || AsianCat.prototype instanceof Animal;

console.log("AsianCat is child of Animal? " + isSubClass1); // true

let isSubClass2 = AsianCat === Animal || Animal.prototype instanceof AsianCat;

console.log("Animal is child of AsianCat? " + isSubClass2); // false

 

5- Đa hình với hàm

Các ngôn ngữ như Java, C# rất chặt chẽ về kiểu dữ liệu. Vì vậy khi bạn gọi một phương thức (Hàm) bạn phải truyền vào đúng kiểu dữ liệu ứng với các tham số. Trong ECMAScript khi gọi một hàm bạn có thể truyền vào tham số với kiểu dữ liệu bất kỳ, rủi ro có thể xẩy ra, và người lập trình phải tự quản lý các rủi ro đó.
Ở đây tôi tạo ra 2 lớp EnglishFrench. Cả hai lớp này đều có phương thức greeting(). Cả hai tạo ra các lời chào khác nhau. Tạo ra 2 đối tượng tương ứng từ 2 lớp trên và gọi hành động của 2 đối tượng này trong cùng một hàm (Hàm intro)
polymorphism-example.js
class English {

    greeting() {
        console.log("Hello");
    }
}

class French {
    greeting() {
        console.log("Bonjour");
    }
}

function intro(language) {
    language.greeting();
}

// -------------- TEST ----------------------

let flora = new English();
let aalase = new French();

// Call function:
intro(flora);
intro(aalase);

let someObject = {};

intro(someObject);// Error!!!
Chạy ví dụ:
OK, Sửa code của ví dụ trên, thêm vào các kiểm tra cần thiết để tránh các rủi ro cho ứng dụng của bạn:
polymorphism-example2.js
class English {

    greeting() {
        console.log("Hello");
    }
}

class French {
    greeting() {
        console.log("Bonjour");
    }
}

function intro(language) {
    // Check type of 'language' object.
    if(language instanceof English || language instanceof French)  {
      language.greeting();
    }
}

// -------------- TEST ----------------------

let flora = new English();
let aalase = new French();

// Call function:
intro(flora);
intro(aalase);

let someObject = {};

intro(someObject);
Áp dụng kiến trúc "Lớp & Thừa kế" giúp ứng dụng của bạn dễ dàng quản lý hơn và tránh các sai sót.
polymorphism-example3.js
// A Base class
class Language {

}
class English extends Language {

    greeting() {
        console.log("Hello");
    }
}

class French  extends Language {
    greeting() {
        console.log("Bonjour");
    }
}

function intro(language) {
    // Check type of 'language' object.
    if(language instanceof Language)  {
      language.greeting();
    }
}

// -------------- TEST ----------------------

let flora = new English();
let aalase = new French();

// Call function:
intro(flora);
intro(aalase);

let someObject = {};

intro(someObject);
Xem thêm:

Xem thêm các chuyên mục: