Tôi có khá nhiều chức năng để cho phép các lớp được định nghĩa với nhiều kế thừa. Nó cho phép mã như sau. Nhìn chung, bạn sẽ lưu ý một sự khởi đầu hoàn toàn từ các kỹ thuật Phân loại gốc trong javascript (ví dụ: bạn sẽ không bao giờ thấy classtừ khóa):
let human = new Running({ name: 'human', numLegs: 2 });
human.run();
let airplane = new Flying({ name: 'airplane', numWings: 2 });
airplane.fly();
let dragon = new RunningFlying({ name: 'dragon', numLegs: 4, numWings: 6 });
dragon.takeFlight();
để tạo ra sản lượng như thế này:
human runs with 2 legs.
airplane flies away with 2 wings!
dragon runs with 4 legs.
dragon flies away with 6 wings!
Dưới đây là các định nghĩa lớp trông như thế nào:
let Named = makeClass('Named', {}, () => ({
init: function({ name }) {
this.name = name;
}
}));
let Running = makeClass('Running', { Named }, protos => ({
init: function({ name, numLegs }) {
protos.Named.init.call(this, { name });
this.numLegs = numLegs;
},
run: function() {
console.log(`${this.name} runs with ${this.numLegs} legs.`);
}
}));
let Flying = makeClass('Flying', { Named }, protos => ({
init: function({ name, numWings }) {
protos.Named.init.call(this, { name });
this.numWings = numWings;
},
fly: function( ){
console.log(`${this.name} flies away with ${this.numWings} wings!`);
}
}));
let RunningFlying = makeClass('RunningFlying', { Running, Flying }, protos => ({
init: function({ name, numLegs, numWings }) {
protos.Running.init.call(this, { name, numLegs });
protos.Flying.init.call(this, { name, numWings });
},
takeFlight: function() {
this.run();
this.fly();
}
}));
Chúng ta có thể thấy rằng mỗi định nghĩa lớp sử dụng makeClass hàm chấp nhận một Objecttên lớp cha được ánh xạ tới các lớp cha. Nó cũng chấp nhận một hàm trả về một Objectthuộc tính chứa cho lớp được định nghĩa. Hàm này có một tham số protos, chứa đủ thông tin để truy cập vào bất kỳ thuộc tính nào được xác định bởi bất kỳ lớp cha nào.
Phần cuối cùng được yêu cầu là makeClasschính hàm, nó thực hiện khá nhiều công việc. Đây là, cùng với phần còn lại của mã. Tôi đã nhận xét makeClasskhá nhiều:
let makeClass = (name, parents={}, propertiesFn=()=>({})) => {
// The constructor just curries to a Function named "init"
let Class = function(...args) { this.init(...args); };
// This allows instances to be named properly in the terminal
Object.defineProperty(Class, 'name', { value: name });
// Tracking parents of `Class` allows for inheritance queries later
Class.parents = parents;
// Initialize prototype
Class.prototype = Object.create(null);
// Collect all parent-class prototypes. `Object.getOwnPropertyNames`
// will get us the best results. Finally, we'll be able to reference
// a property like "usefulMethod" of Class "ParentClass3" with:
// `parProtos.ParentClass3.usefulMethod`
let parProtos = {};
for (let parName in parents) {
let proto = parents[parName].prototype;
parProtos[parName] = {};
for (let k of Object.getOwnPropertyNames(proto)) {
parProtos[parName][k] = proto[k];
}
}
// Resolve `properties` as the result of calling `propertiesFn`. Pass
// `parProtos`, so a child-class can access parent-class methods, and
// pass `Class` so methods of the child-class have a reference to it
let properties = propertiesFn(parProtos, Class);
properties.constructor = Class; // Ensure "constructor" prop exists
// If two parent-classes define a property under the same name, we
// have a "collision". In cases of collisions, the child-class *must*
// define a method (and within that method it can decide how to call
// the parent-class methods of the same name). For every named
// property of every parent-class, we'll track a `Set` containing all
// the methods that fall under that name. Any `Set` of size greater
// than one indicates a collision.
let propsByName = {}; // Will map property names to `Set`s
for (let parName in parProtos) {
for (let propName in parProtos[parName]) {
// Now track the property `parProtos[parName][propName]` under the
// label of `propName`
if (!propsByName.hasOwnProperty(propName))
propsByName[propName] = new Set();
propsByName[propName].add(parProtos[parName][propName]);
}
}
// For all methods defined by the child-class, create or replace the
// entry in `propsByName` with a Set containing a single item; the
// child-class' property at that property name (this also guarantees
// there is no collision at this property name). Note property names
// prefixed with "$" will be considered class properties (and the "$"
// will be removed).
for (let propName in properties) {
if (propName[0] === '$') {
// The "$" indicates a class property; attach to `Class`:
Class[propName.slice(1)] = properties[propName];
} else {
// No "$" indicates an instance property; attach to `propsByName`:
propsByName[propName] = new Set([ properties[propName] ]);
}
}
// Ensure that "init" is defined by a parent-class or by the child:
if (!propsByName.hasOwnProperty('init'))
throw Error(`Class "${name}" is missing an "init" method`);
// For each property name in `propsByName`, ensure that there is no
// collision at that property name, and if there isn't, attach it to
// the prototype! `Object.defineProperty` can ensure that prototype
// properties won't appear during iteration with `in` keyword:
for (let propName in propsByName) {
let propsAtName = propsByName[propName];
if (propsAtName.size > 1)
throw new Error(`Class "${name}" has conflict at "${propName}"`);
Object.defineProperty(Class.prototype, propName, {
enumerable: false,
writable: true,
value: propsAtName.values().next().value // Get 1st item in Set
});
}
return Class;
};
let Named = makeClass('Named', {}, () => ({
init: function({ name }) {
this.name = name;
}
}));
let Running = makeClass('Running', { Named }, protos => ({
init: function({ name, numLegs }) {
protos.Named.init.call(this, { name });
this.numLegs = numLegs;
},
run: function() {
console.log(`${this.name} runs with ${this.numLegs} legs.`);
}
}));
let Flying = makeClass('Flying', { Named }, protos => ({
init: function({ name, numWings }) {
protos.Named.init.call(this, { name });
this.numWings = numWings;
},
fly: function( ){
console.log(`${this.name} flies away with ${this.numWings} wings!`);
}
}));
let RunningFlying = makeClass('RunningFlying', { Running, Flying }, protos => ({
init: function({ name, numLegs, numWings }) {
protos.Running.init.call(this, { name, numLegs });
protos.Flying.init.call(this, { name, numWings });
},
takeFlight: function() {
this.run();
this.fly();
}
}));
let human = new Running({ name: 'human', numLegs: 2 });
human.run();
let airplane = new Flying({ name: 'airplane', numWings: 2 });
airplane.fly();
let dragon = new RunningFlying({ name: 'dragon', numLegs: 4, numWings: 6 });
dragon.takeFlight();
Các makeClasschức năng cũng hỗ trợ thuộc tính của lớp; chúng được xác định bằng tiền tố tên thuộc tính với $ký hiệu (lưu ý rằng tên thuộc tính cuối cùng mà kết quả sẽ bị $xóa). Với suy nghĩ này, chúng tôi có thể viết một chuyên ngànhDragon lớp mô hình hóa "loại" của Rồng, trong đó danh sách các loại Rồng có sẵn được lưu trữ trên chính Lớp đó, trái ngược với các trường hợp:
let Dragon = makeClass('Dragon', { RunningFlying }, protos => ({
$types: {
wyvern: 'wyvern',
drake: 'drake',
hydra: 'hydra'
},
init: function({ name, numLegs, numWings, type }) {
protos.RunningFlying.init.call(this, { name, numLegs, numWings });
this.type = type;
},
description: function() {
return `A ${this.type}-type dragon with ${this.numLegs} legs and ${this.numWings} wings`;
}
}));
let dragon1 = new Dragon({ name: 'dragon1', numLegs: 2, numWings: 4, type: Dragon.types.drake });
let dragon2 = new Dragon({ name: 'dragon2', numLegs: 4, numWings: 2, type: Dragon.types.hydra });
Những thách thức của đa kế thừa
Bất cứ ai theo dõi mã makeClasschặt chẽ sẽ lưu ý một hiện tượng không mong muốn khá quan trọng xảy ra âm thầm khi đoạn mã trên chạy: khởi tạo một RunningFlyingkết quả sẽ dẫn đến HAI cuộc gọi đến nhà Namedxây dựng!
Điều này là do biểu đồ thừa kế trông như thế này:
(^^ More Specialized ^^)
RunningFlying
/ \
/ \
Running Flying
\ /
\ /
Named
(vv More Abstract vv)
Khi có nhiều đường dẫn đến cùng một lớp cha trong biểu đồ thừa kế của lớp con , các cảnh báo của lớp con sẽ gọi hàm tạo của lớp cha mẹ đó nhiều lần.
Kết hợp điều này là không tầm thường. Chúng ta hãy xem xét một số ví dụ với các tên lớp đơn giản hóa. Chúng ta sẽ xem xét lớp A, lớp cha mẹ trừu tượng nhất, các lớp Bvà C, cả hai đều kế thừa từ Avà lớp BCkế thừa từ Bvà C(và do đó về mặt khái niệm là "thừa kế kép" từ A):
let A = makeClass('A', {}, () => ({
init: function() {
console.log('Construct A');
}
}));
let B = makeClass('B', { A }, protos => ({
init: function() {
protos.A.init.call(this);
console.log('Construct B');
}
}));
let C = makeClass('C', { A }, protos => ({
init: function() {
protos.A.init.call(this);
console.log('Construct C');
}
}));
let BC = makeClass('BC', { B, C }, protos => ({
init: function() {
// Overall "Construct A" is logged twice:
protos.B.init.call(this); // -> console.log('Construct A'); console.log('Construct B');
protos.C.init.call(this); // -> console.log('Construct A'); console.log('Construct C');
console.log('Construct BC');
}
}));
Nếu chúng ta muốn ngăn chặn việc BCgọi hai lầnA.prototype.init chúng ta có thể cần phải từ bỏ kiểu gọi trực tiếp các hàm tạo. Chúng tôi sẽ cần một số mức độ gián tiếp để kiểm tra xem các cuộc gọi trùng lặp có xảy ra hay không và ngắn mạch trước khi chúng xảy ra.
Chúng ta có thể xem xét thay đổi các tham số được cung cấp cho hàm thuộc tính: bên cạnh protos, Objectchứa dữ liệu thô mô tả các thuộc tính được kế thừa, chúng ta cũng có thể bao gồm một hàm tiện ích để gọi một phương thức cá thể theo cách mà các phương thức cha mẹ cũng được gọi, nhưng các cuộc gọi trùng lặp được phát hiện và ngăn chặn. Chúng ta hãy xem nơi chúng ta thiết lập các tham số cho propertiesFn Function:
let makeClass = (name, parents, propertiesFn) => {
/* ... a bunch of makeClass logic ... */
// Allows referencing inherited functions; e.g. `parProtos.ParentClass3.usefulMethod`
let parProtos = {};
/* ... collect all parent methods in `parProtos` ... */
// Utility functions for calling inherited methods:
let util = {};
util.invokeNoDuplicates = (instance, fnName, args, dups=new Set()) => {
// Invoke every parent method of name `fnName` first...
for (let parName of parProtos) {
if (parProtos[parName].hasOwnProperty(fnName)) {
// Our parent named `parName` defines the function named `fnName`
let fn = parProtos[parName][fnName];
// Check if this function has already been encountered.
// This solves our duplicate-invocation problem!!
if (dups.has(fn)) continue;
dups.add(fn);
// This is the first time this Function has been encountered.
// Call it on `instance`, with the desired args. Make sure we
// include `dups`, so that if the parent method invokes further
// inherited methods we don't lose track of what functions have
// have already been called.
fn.call(instance, ...args, dups);
}
}
};
// Now we can call `propertiesFn` with an additional `util` param:
// Resolve `properties` as the result of calling `propertiesFn`:
let properties = propertiesFn(parProtos, util, Class);
/* ... a bunch more makeClass logic ... */
};
Toàn bộ mục đích của thay đổi ở trên makeClasslà để chúng tôi có thêm một đối số được cung cấp cho chúng tôi propertiesFnkhi chúng tôi gọi makeClass. Chúng ta cũng nên lưu ý rằng mọi hàm được định nghĩa trong bất kỳ lớp nào bây giờ có thể nhận được một tham số sau tất cả các hàm khác, được đặt tên dup, đó là một Sethàm chứa tất cả các hàm đã được gọi là kết quả của việc gọi phương thức được kế thừa:
let A = makeClass('A', {}, () => ({
init: function() {
console.log('Construct A');
}
}));
let B = makeClass('B', { A }, (protos, util) => ({
init: function(dups) {
util.invokeNoDuplicates(this, 'init', [ /* no args */ ], dups);
console.log('Construct B');
}
}));
let C = makeClass('C', { A }, (protos, util) => ({
init: function(dups) {
util.invokeNoDuplicates(this, 'init', [ /* no args */ ], dups);
console.log('Construct C');
}
}));
let BC = makeClass('BC', { B, C }, (protos, util) => ({
init: function(dups) {
util.invokeNoDuplicates(this, 'init', [ /* no args */ ], dups);
console.log('Construct BC');
}
}));
Phong cách mới này thực sự thành công trong việc đảm bảo "Construct A"chỉ được ghi lại một lần khi một phiên bản BCđược khởi tạo. Nhưng có ba nhược điểm, thứ ba là rất nghiêm trọng :
- Mã này đã trở nên ít đọc và có thể duy trì. Rất nhiều sự phức tạp ẩn đằng sau
util.invokeNoDuplicateschức năng, và suy nghĩ về cách phong cách này tránh được việc gọi đa năng là không trực quan và gây đau đầu. Chúng ta cũng có dupstham số pesky đó, thực sự cần được xác định trên mỗi hàm duy nhất trong lớp . Ôi.
- Mã này chậm hơn - yêu cầu nhiều hơn một chút và tính toán để đạt được kết quả mong muốn với nhiều kế thừa. Thật không may, đây có thể là trường hợp với bất kỳ giải pháp nào cho vấn đề đa yêu cầu của chúng tôi.
- Đáng kể nhất, cấu trúc của các chức năng dựa trên sự kế thừa đã trở nên rất cứng nhắc . Nếu một lớp con
NiftyClassghi đè một hàm niftyFunctionvà sử dụng util.invokeNoDuplicates(this, 'niftyFunction', ...)để chạy nó mà không cần gọi trùng lặp, NiftyClass.prototype.niftyFunctionsẽ gọi hàm có tên niftyFunctioncủa mọi lớp cha định nghĩa nó, bỏ qua mọi giá trị trả về từ các lớp đó và cuối cùng thực hiện logic chuyên biệt của NiftyClass.prototype.niftyFunction. Đây là cấu trúc duy nhất có thể . Nếu NiftyClasskế thừa CoolClassvà GoodClassvà cả hai lớp cha mẹ này cung cấp các niftyFunctionđịnh nghĩa của riêng chúng, NiftyClass.prototype.niftyFunctionsẽ không bao giờ (không có rủi ro nhiều lần gọi) có thể:
- A. Chạy logic chuyên biệt
NiftyClasstrước, sau đó là logic chuyên biệt của các lớp cha
- B. Chạy logic chuyên biệt
NiftyClasstại bất kỳ điểm nào khác ngoài sau khi tất cả logic cha chuyên biệt đã hoàn thành
- C. Hành xử có điều kiện tùy thuộc vào giá trị trả về của logic chuyên biệt của cha mẹ
- D. Tránh chạy
niftyFunctionhoàn toàn chuyên biệt của một phụ huynh cụ thể
Tất nhiên, chúng ta có thể giải quyết từng vấn đề bằng chữ ở trên bằng cách xác định các hàm chuyên dụng trong util:
- A. xác định
util.invokeNoDuplicatesSubClassLogicFirst(instance, fnName, ...)
- B. xác định
util.invokeNoDuplicatesSubClassAfterParent(parentName, instance, fnName, ...)( parentNameTên của cha mẹ có logic chuyên biệt sẽ được theo sau bởi logic chuyên biệt của lớp con)
- C. xác định
util.invokeNoDuplicatesCanShortCircuitOnParent(parentName, testFn, instance, fnName, ...)(Trong trường hợp này testFnsẽ nhận được kết quả của logic chuyên biệt cho cha mẹ được đặt tên parentNamevà sẽ trả về một true/falsegiá trị cho biết liệu có xảy ra ngắn mạch hay không)
- D. xác định
util.invokeNoDuplicatesBlackListedParents(blackList, instance, fnName, ...)(Trong trường hợp này blackListsẽ là một Arraytên cha mẹ có logic chuyên biệt nên được bỏ qua hoàn toàn)
Những giải pháp này đều có sẵn, nhưng đây là tình trạng hỗn loạn ! Đối với mọi cấu trúc duy nhất mà một lệnh gọi hàm được kế thừa có thể thực hiện, chúng ta sẽ cần một phương thức chuyên biệt được định nghĩa dưới đây util. Thật là một thảm họa tuyệt đối.
Với suy nghĩ này, chúng ta có thể bắt đầu thấy những thách thức trong việc thực hiện nhiều kế thừa tốt. Việc thực hiện đầy đủ của makeClasstôi được cung cấp trong câu trả lời này thậm chí không xem xét vấn đề đa yêu cầu, hoặc nhiều vấn đề khác phát sinh liên quan đến nhiều kế thừa.
Câu trả lời này đang trở nên rất dài. Tôi hy vọng việc makeClasstriển khai tôi đưa vào vẫn hữu ích, ngay cả khi nó không hoàn hảo. Tôi cũng hy vọng bất cứ ai quan tâm đến chủ đề này sẽ có được nhiều bối cảnh hơn để ghi nhớ khi họ đọc thêm!