Thực hiện mô hình kế thừa ECMAScript

Thực hiện cơ chế kế thừa

Để thực hiện cơ chế kế thừa bằng ECMAScript, bạn có thể bắt đầu từ lớp cơ sở mà bạn muốn kế thừa. Tất cả các lớp được định nghĩa bởi nhà phát triển đều có thể là lớp cơ sở. Do lý do an toàn, các lớp cục bộ và lớp chủ không thể là lớp cơ sở, điều này giúp ngăn chặn việc truy cập công cộng vào mã đã biên dịch của trình duyệt cấp, vì mã này có thể được sử dụng để tấn công xấu.

Sau khi chọn lớp cơ sở, bạn có thể tạo lớp con của nó. Việc sử dụng lớp cơ sở hoàn toàn do bạn quyết định. Có thể bạn muốn tạo một lớp cơ sở không thể sử dụng trực tiếp, mà chỉ để cung cấp các hàm chung cho lớp con. Trong trường hợp này, lớp cơ sở được coi là lớp trừu tượng.

Mặc dù ECMAScript không định nghĩa chặt chẽ lớp trừu tượng như các ngôn ngữ khác, nhưng đôi khi nó thực sự tạo ra một số lớp không được phép sử dụng. Thường thì chúng ta gọi loại lớp này là lớp trừu tượng.

Đối tượng con sẽ kế thừa tất cả các thuộc tính và phương pháp của đối tượng cha, bao gồm cả việc thực hiện hàm xây dựng và các phương pháp. Hãy nhớ rằng tất cả các thuộc tính và phương pháp đều là công cộng, vì vậy đối tượng con có thể truy cập trực tiếp vào các phương pháp này. Đối tượng con cũng có thể thêm các thuộc tính và phương pháp mới mà đối tượng cha không có, hoặc có thể ghi đè các thuộc tính và phương pháp của đối tượng cha.

Cách thức kế thừa

Như các chức năng khác, cách thực hiện kế thừa trong ECMAScript không chỉ một cách. Điều này là vì cơ chế kế thừa trong JavaScript không được quy định rõ ràng, mà thông qua việc mô phỏng. Điều này có nghĩa là không phải tất cả các chi tiết kế thừa đều được xử lý hoàn toàn bởi trình giải thích. Là nhà phát triển, bạn có quyền quyết định cách kế thừa phù hợp nhất.

Dưới đây là một số cách kế thừa cụ thể.

Đối tượng giả mạo

Khi tưởng tượng ra ECMAScript ban đầu, không có ý định thiết kế đối tượng giả mạo (object masquerading). Nó phát triển sau khi các nhà phát triển bắt đầu hiểu cách làm việc của hàm, đặc biệt là cách sử dụng từ khóa this trong môi trường hàm.

Nguyên lý như sau: hàm构造函数 sử dụng từ khóa this để gán giá trị cho tất cả các thuộc tính và phương thức (tức là sử dụng cách thức khai báo hàm của lớp). Vì hàm构造函数 chỉ là một hàm, vì vậy có thể sử dụng hàm构造函数 của ClassA như một phương thức của ClassB, sau đó gọi nó. ClassB sẽ nhận được các thuộc tính và phương thức được định nghĩa trong hàm构造函数 của ClassA. Ví dụ, định nghĩa ClassA và ClassB như sau:

function ClassA(sColor) {
    this.color = sColor;
    this.sayColor = function () {
        alert(this.color);
    };
}
function ClassB(sColor) {
}

Bạn còn nhớ không? Từ khóa this chỉ vào đối tượng được tạo bởi hàm构造函数 hiện tại. Tuy nhiên trong phương pháp này, this chỉ vào đối tượng thuộc về. Nguyên lý này là sử dụng ClassA như một hàm thông thường để xây dựng cơ chế kế thừa,而不是 như một hàm构造函数. Cách sử dụng hàm构造函数 ClassB để thực hiện cơ chế kế thừa như sau:

function ClassB(sColor) {
    this.newMethod = ClassA;
    this.newMethod(sColor);
    delete this.newMethod;
}

Trong đoạn mã này, phương thức newMethod của ClassA được gán (hãy nhớ rằng tên hàm chỉ là con trỏ đến nó). Sau đó, gọi phương thức này, truyền cho nó là tham số của hàm构造函数 ClassB. Dòng mã cuối cùng xóa bỏ tham chiếu đến ClassA, vì vậy sau này không thể gọi nó nữa.

Tất cả các thuộc tính và phương thức mới phải được định nghĩa sau khi xóa dòng mã định nghĩa phương thức mới. Nếu không, có thể sẽ chèn盖 lớp cha liên quan:

function ClassB(sColor, sName) {
    this.newMethod = ClassA;
    this.newMethod(sColor);
    delete this.newMethod;
    this.name = sName;
    this.sayName = function () {
        alert(this.name);
    };
}

Để chứng minh mã trước đó hoạt động, có thể chạy ví dụ sau:

var objA = new ClassA("blue");
var objB = new ClassB("red", "John");
objA.sayColor();	//Xuất ra "blue"
objB.sayColor();	//Xuất ra "red"
objB.sayName();		// Xuất ra "John"

Đối tượng giả mạo có thể thực hiện kế thừa đa cấp

Điều thú vị là, việc đối tượng giả mạo có thể hỗ trợ kế thừa đa cấp. Nghĩa là, một lớp có thể kế thừa nhiều lớp cha. Mekhân chế kế thừa đa cấp được biểu diễn bằng UML như hình sau:

Mô hình kế thừa UML ví dụ minh họa

Ví dụ, nếu có hai lớp ClassX và ClassY, ClassZ muốn kế thừa hai lớp này, có thể sử dụng mã sau:

function ClassZ() {
    this.newMethod = ClassX;
    this.newMethod();
    delete this.newMethod;
    this.newMethod = ClassY;
    this.newMethod();
    delete this.newMethod;
}

TIY

Có một nhược điểm, nếu hai lớp ClassX và ClassY có thuộc tính hoặc phương thức cùng tên, ClassY có ưu tiên cao hơn vì nó kế thừa từ lớp sau. Ngoài vấn đề nhỏ này ra, việc thực hiện cơ chế kế thừa đa cấp bằng cách假冒 đối tượng rất dễ dàng.

Do sự phổ biến của phương thức kế thừa này, ECMAScript phiên bản thứ ba đã thêm hai phương thức vào đối tượng Function, đó là call() và apply().

call() phương thức

call() phương thức là phương thức tương tự nhất với phương thức假冒 đối tượng cổ điển. Tham số đầu tiên được sử dụng làm đối tượng của this. Các tham số khác được truyền trực tiếp vào hàm. Ví dụ:

function sayColor(sPrefix,sSuffix) {
    alert(sPrefix + this.color + sSuffix);
};
var obj = new Object();
obj.color = "blue";
sayColor.call(obj, "The color is ", "a very nice color indeed.");

Trong ví dụ này, hàm sayColor() được định nghĩa ngoài đối tượng, ngay cả khi nó không thuộc về bất kỳ đối tượng nào, vẫn có thể truy cập từ khóa this. Thuộc tính color của đối tượng obj bằng blue. Khi gọi phương thức call(), tham số đầu tiên là obj, có nghĩa là cần gán giá trị của từ khóa this trong hàm sayColor() là obj. Tham số thứ hai và thứ ba là các chuỗi. Chúng tương ứng với tham số sPrefix và sSuffix trong hàm sayColor(), và thông điệp được tạo ra "The color is blue, a very nice color indeed." sẽ được hiển thị ra.

Để sử dụng phương thức假冒 đối tượng với cơ chế kế thừa, chỉ cần thay thế mã gán giá trị, gọi và xóa ba dòng đầu tiên:

function ClassB(sColor, sName) {
    //this.newMethod = ClassA;
    //this.newMethod(color);
    //delete this.newMethod;
    ClassA.call(this, sColor);
    this.name = sName;
    this.sayName = function () {
        alert(this.name);
    };
}

TIY

Ở đây, chúng ta cần để từ khóa this trong ClassA bằng đối tượng ClassB mới tạo, vì vậy this là tham số đầu tiên. Tham số thứ hai sColor là tham số duy nhất cho cả hai lớp.

apply() phương thức

apply() phương thức có hai tham số, được sử dụng làm đối tượng của this và mảng các tham số cần truyền vào hàm. Ví dụ:

function sayColor(sPrefix,sSuffix) {
    alert(sPrefix + this.color + sSuffix);
};
var obj = new Object();
obj.color = "blue";
sayColor.apply(obj, new Array("The color is ", "a very nice color indeed."));

Ví dụ này tương tự như ví dụ trước, chỉ khác là bây giờ gọi phương thức apply(). Khi gọi phương thức apply(), tham số đầu tiên vẫn là obj, có nghĩa là cần gán giá trị cho từ khóa this trong hàm sayColor() là obj. Tham số thứ hai là một mảng chứa hai chuỗi, khớp với các tham số sPrefix và sSuffix trong hàm sayColor(), thông điệp cuối cùng vẫn là "The color is blue, a very nice color indeed." và sẽ được hiển thị ra.

Phương thức này cũng được sử dụng để thay thế mã gán giá trị, gọi và xóa phương thức mới trong ba dòng trước:

function ClassB(sColor, sName) {
    //this.newMethod = ClassA;
    //this.newMethod(color);
    //delete this.newMethod;
    ClassA.apply(this, new Array(sColor));
    this.name = sName;
    this.sayName = function () {
        alert(this.name);
    };
}

Cũng vậy, tham số đầu tiên vẫn là this, tham số thứ hai là một mảng duy nhất chứa giá trị color. Bạn có thể truyền đối tượng arguments toàn bộ của ClassB như tham số thứ hai cho phương thức apply():

function ClassB(sColor, sName) {
    //this.newMethod = ClassA;
    //this.newMethod(color);
    //delete this.newMethod;
    ClassA.apply(this, arguments);
    this.name = sName;
    this.sayName = function () {
        alert(this.name);
    };
}

TIY

Tất nhiên, chỉ khi thứ tự các tham số trong lớp cha hoàn toàn trùng khớp với thứ tự các tham số trong lớp con thì mới có thể truyền đối tượng tham số. Nếu không, bạn phải tạo một mảng riêng biệt và đặt các tham số theo thứ tự đúng. Ngoài ra, bạn cũng có thể sử dụng phương thức call().

Chuỗi prototype (prototype chaining)

Hình thức kế thừa này trong ECMAScript ban đầu được sử dụng cho chuỗi prototype. Chương trước đã giới thiệu về cách định nghĩa lớp bằng phương thức prototype. Chuỗi prototype mở rộng cách này để thực hiện cơ chế kế thừa một cách thú vị.

Như đã học trong chương trước, đối tượng prototype là một mẫu, tất cả các đối tượng cần được tạo ra đều dựa trên mẫu này. Tóm lại, bất kỳ thuộc tính hoặc phương thức nào của đối tượng prototype đều được truyền sang tất cả các đối tượng của lớp đó. Hệ thống chuỗi prototype sử dụng chức năng này để thực hiện cơ chế kế thừa.

Nếu định nghĩa lại lớp trước đó bằng cách sử dụng phương thức prototype, chúng sẽ trở thành hình thức sau:

function ClassA() {
}
ClassA.prototype.color = "blue";
ClassA.prototype.sayColor = function () {
    alert(this.color);
};
function ClassB() {
}
ClassB.prototype = new ClassA();

Điểm đặc biệt của phương thức prototype là sự nhấn mạnh vào dòng mã màu xanh. Ở đây, đặt thuộc tính prototype của ClassB thành instance của ClassA. Điều này rất thú vị vì muốn có tất cả các thuộc tính và phương thức của ClassA, nhưng không muốn thêm chúng vào thuộc tính prototype của ClassB. Có phải có cách nào tốt hơn để đặt instance của ClassA vào thuộc tính prototype không?

Lưu ý:Gọi hàm constructor của ClassA, không truyền bất kỳ tham số nào. Điều này là cách làm tiêu chuẩn trong prototype chain. Đảm bảo rằng hàm constructor không có bất kỳ tham số nào.

Giống như cách đối tượng giả mạo, tất cả các thuộc tính và phương thức của lớp con phải xuất hiện sau khi thuộc tính prototype được gán giá trị, vì tất cả các phương thức được gán giá trị trước đó sẽ bị xóa bỏ. Tại sao? Bởi vì thuộc tính prototype được thay thế bằng đối tượng mới, đối tượng ban đầu chứa phương thức sẽ bị hủy bỏ. Do đó, mã để thêm thuộc tính name và phương thức sayName() cho lớp ClassB như sau:

function ClassB() {
}
ClassB.prototype = new ClassA();
ClassB.prototype.name = "";
ClassB.prototype.sayName = function () {
    alert(this.name);
};

Bạn có thể kiểm tra đoạn mã này bằng cách chạy ví dụ sau:

var objA = new ClassA();
var objB = new ClassB();
objA.color = "blue";
objB.color = "red";
objB.name = "John";
objA.sayColor();
objB.sayColor();
objB.sayName();

TIY

Ngoài ra, trong prototype chain, cách thức hoạt động của toán tử instanceof cũng rất đặc biệt. Đối với tất cả các instance của ClassB, toán tử instanceof sẽ trả về true cho cả ClassA và ClassB. Ví dụ:

var objB = new ClassB();
alert(objB instanceof ClassA);	// Xuất ra "true"
alert(objB instanceof ClassB);	// Xuất ra "true"

Trong thế giới弱类型 của ECMAScript, điều này là một công cụ rất hữu ích, nhưng không thể sử dụng nó khi đối tượng giả mạo.

Lợi ích của prototype chain là không hỗ trợ kế thừa đa重. Nhớ rằng, prototype chain sẽ sử dụng đối tượng của một loại khác để ghi đè thuộc tính prototype của lớp.

Cách trộn lẫn

Phương thức kế thừa này sử dụng hàm constructor để định nghĩa lớp, không sử dụng bất kỳ prototype nào. Vấn đề chính của cách đối tượng giả mạo là phải sử dụng cách hàm constructor, điều này không phải là lựa chọn tốt nhất. Tuy nhiên, nếu sử dụng prototype chain, bạn sẽ không thể sử dụng hàm constructor có tham số. Nhà phát triển nên chọn như thế nào? Câu trả lời rất đơn giản, cả hai đều sử dụng.

Trong chương trước, chúng ta đã giải thích rằng cách tốt nhất để tạo lớp là sử dụng hàm khởi tạo để định nghĩa thuộc tính, sử dụng prototype để định nghĩa phương pháp. Cách này cũng áp dụng cho cơ chế kế thừa, sử dụng đối tượng để kế thừa thuộc tính của hàm khởi tạo, sử dụng chuỗi prototype để kế thừa phương pháp của đối tượng prototype. Sử dụng hai cách này để viết lại ví dụ trước, mã như sau:

function ClassA(sColor) {
    this.color = sColor;
}
ClassA.prototype.sayColor = function () {
    alert(this.color);
};
function ClassB(sColor, sName) {
    ClassA.call(this, sColor);
    this.name = sName;
}
ClassB.prototype = new ClassA();
ClassB.prototype.sayName = function () {
    alert(this.name);
};

Trong ví dụ này, cơ chế kế thừa được thực hiện bởi hai dòng mã được nhấn mạnh màu xanh lam. Trong dòng mã được nhấn mạnh màu xanh lam đầu tiên, trong hàm khởi tạo ClassB, sử dụng đối tượng để kế thừa thuộc tính sColor của lớp ClassA. Trong dòng mã được nhấn mạnh màu xanh lam thứ hai, sử dụng chuỗi prototype để kế thừa phương pháp của lớp ClassA. Do sử dụng chuỗi prototype, nên toán tử instanceof vẫn hoạt động chính xác.

Dưới đây là ví dụ kiểm tra đoạn mã này:

var objA = new ClassA("blue");
var objB = new ClassB("red", "John");
objA.sayColor();	//Xuất ra "blue"
objB.sayColor();	//Xuất ra "red"
objB.sayName();	//Xuất ra "John"

TIY