Saya memiliki cukup fungsi untuk memungkinkan kelas didefinisikan dengan multiple inheritance. Ini memungkinkan untuk kode seperti berikut. Secara keseluruhan Anda akan mencatat keberangkatan lengkap dari teknik Klasifikasi asli dalam javascript (mis. Anda tidak akan pernah melihat class
kata kunci):
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();
untuk menghasilkan output seperti ini:
human runs with 2 legs.
airplane flies away with 2 wings!
dragon runs with 4 legs.
dragon flies away with 6 wings!
Berikut adalah definisi kelasnya:
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();
}
}));
Kita dapat melihat bahwa setiap definisi kelas menggunakan makeClass
fungsi menerima Object
nama kelas induk yang dipetakan ke kelas induk. Ini juga menerima fungsi yang mengembalikan Object
properti yang mengandung untuk kelas yang sedang didefinisikan. Fungsi ini memiliki parameterprotos
, yang berisi informasi yang cukup untuk mengakses properti apa pun yang ditentukan oleh kelas induknya.
Bagian terakhir yang diperlukan adalah makeClass
fungsi itu sendiri, yang melakukan sedikit pekerjaan. Ini dia, bersama dengan sisa kode. Saya sudah berkomentar makeClass
cukup banyak:
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();
The makeClass
Fungsi juga mendukung properti kelas; ini didefinisikan dengan awalan nama properti dengan $
simbol (perhatikan bahwa nama properti akhir yang hasilnya akan $
dihapus). Dengan mengingat hal ini, kita dapat menulis Dragon
kelas khusus yang memodelkan "tipe" Naga, di mana daftar tipe Naga yang tersedia disimpan di Kelas itu sendiri, sebagai lawan dari contoh:
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 });
Tantangan Berbagai Warisan
Siapa pun yang mengikuti kode untuk makeClass
erat akan mencatat fenomena yang tidak diinginkan yang agak signifikan terjadi secara diam-diam ketika kode di atas berjalan: instantiating RunningFlying
akan menghasilkan DUA panggilan ke Named
konstruktor!
Ini karena grafik warisan terlihat seperti ini:
(^^ More Specialized ^^)
RunningFlying
/ \
/ \
Running Flying
\ /
\ /
Named
(vv More Abstract vv)
Ketika ada beberapa lintasan ke kelas induk yang sama dalam grafik warisan kelas-sub , instantiations dari sub-kelas akan memanggil konstruktor kelas induk beberapa kali.
Memerangi ini tidak sepele. Mari kita lihat beberapa contoh dengan nama kelas yang disederhanakan. Kami akan mempertimbangkan kelas A
, kelas induk yang paling abstrak, kelas B
dan C
, yang keduanya mewarisi dari A
, dan kelas BC
yang mewarisi dari B
dan C
(dan karenanya secara konseptual "warisan ganda" dari 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');
}
}));
Jika kita ingin mencegah permintaan BC
ganda, A.prototype.init
kita mungkin perlu meninggalkan gaya memanggil langsung konstruktor yang diwariskan. Kita akan memerlukan beberapa tingkat tipuan untuk memeriksa apakah panggilan duplikat terjadi, dan korsleting sebelum terjadi.
Kami dapat mempertimbangkan untuk mengubah parameter yang disediakan ke fungsi properti: di samping protos
, Object
data mentah berisi yang menggambarkan properti yang diwariskan, kami juga dapat menyertakan fungsi utilitas untuk memanggil metode contoh sedemikian rupa sehingga metode induk juga dipanggil, tetapi panggilan duplikat terdeteksi dan dicegah. Mari kita lihat di mana kita menetapkan parameter untuk 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 ... */
};
Seluruh tujuan perubahan di atas makeClass
adalah agar kami memiliki argumen tambahan yang diberikan kepada kami propertiesFn
saat kami memohon makeClass
. Kita juga harus menyadari bahwa setiap fungsi yang didefinisikan dalam kelas apa pun sekarang dapat menerima parameter setelah semua yang lain, dinamai dup
, yang merupakan Set
yang menyimpan semua fungsi yang telah dipanggil sebagai akibat dari memanggil metode yang diwarisi:
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');
}
}));
Gaya baru ini sebenarnya berhasil memastikan "Construct A"
hanya dicatat satu kali ketika sebuah instance BC
diinisialisasi. Tetapi ada tiga kelemahan, yang ketiga sangat kritis :
- Kode ini menjadi kurang mudah dibaca dan dipelihara. Banyak kerumitan bersembunyi di balik
util.invokeNoDuplicates
fungsi, dan berpikir tentang bagaimana gaya ini menghindari multi-doa adalah non-intuitif dan merangsang sakit kepala. Kami juga memiliki dups
parameter sial itu , yang benar-benar perlu didefinisikan pada setiap fungsi tunggal di kelas . Aduh.
- Kode ini lebih lambat - cukup banyak tipuan dan perhitungan diperlukan untuk mencapai hasil yang diinginkan dengan pewarisan berganda. Sayangnya ini mungkin menjadi kasus dengan setiap solusi untuk masalah multiple-doa kita.
- Paling signifikan, struktur fungsi yang bergantung pada warisan menjadi sangat kaku . Jika sub-kelas
NiftyClass
mengabaikan fungsi niftyFunction
, dan menggunakannya util.invokeNoDuplicates(this, 'niftyFunction', ...)
untuk menjalankannya tanpa duplikasi-pemanggilan, NiftyClass.prototype.niftyFunction
akan memanggil fungsi yang dinamai niftyFunction
setiap kelas induk yang mendefinisikannya, mengabaikan nilai pengembalian dari kelas-kelas itu, dan akhirnya melakukan logika khusus NiftyClass.prototype.niftyFunction
. Ini adalah satu - satunya struktur yang mungkin . Jika NiftyClass
mewarisi CoolClass
dan GoodClass
, dan kedua kelas induk ini memberikan niftyFunction
definisi sendiri, NiftyClass.prototype.niftyFunction
tidak akan pernah (tanpa mempertaruhkan banyak pemanggilan) dapat:
- A. Jalankan logika khusus
NiftyClass
pertama, lalu logika khusus kelas induk
- B. Jalankan logika khusus
NiftyClass
pada titik mana pun selain setelah semua logika induk khusus telah selesai
- C. Berperilaku secara kondisional tergantung pada nilai balik logika khusus induknya
- D. Hindari menjalankan orangtua tertentu yang khusus
niftyFunction
sama sekali
Tentu saja, kami dapat menyelesaikan setiap masalah dengan huruf di atas dengan mendefinisikan fungsi khusus di bawah util
:
- A. definisikan
util.invokeNoDuplicatesSubClassLogicFirst(instance, fnName, ...)
- B. define
util.invokeNoDuplicatesSubClassAfterParent(parentName, instance, fnName, ...)
(Di mana parentName
nama induk yang logika khususnya akan segera diikuti oleh logika khusus kelas anak-anak)
- C. define
util.invokeNoDuplicatesCanShortCircuitOnParent(parentName, testFn, instance, fnName, ...)
(Dalam hal ini testFn
akan menerima hasil dari logika khusus untuk orang tua yang bernama parentName
, dan akan mengembalikan true/false
nilai yang menunjukkan apakah korsleting harus terjadi)
- D. define
util.invokeNoDuplicatesBlackListedParents(blackList, instance, fnName, ...)
(Dalam hal ini blackList
akan menjadi Array
nama induk yang logikanasinya harus dilewati sama sekali)
Semua solusi ini tersedia, tetapi ini adalah kekacauan total ! Untuk setiap struktur unik yang dapat diambil oleh panggilan fungsi yang diwarisi, kita membutuhkan metode khusus yang didefinisikan di bawah util
. Benar-benar bencana yang absolut.
Dengan mengingat hal ini, kita dapat mulai melihat tantangan dalam menerapkan pewarisan berganda yang baik. Implementasi penuh dari yang makeClass
saya berikan dalam jawaban ini bahkan tidak mempertimbangkan masalah seruan berganda, atau banyak masalah lain yang timbul berkenaan dengan multiple inheritance.
Jawaban ini semakin panjang. Saya harap makeClass
implementasi yang saya masukkan masih bermanfaat, meskipun tidak sempurna. Saya juga berharap siapa pun yang tertarik dengan topik ini mendapatkan lebih banyak konteks untuk diingat ketika mereka membaca lebih lanjut!