Sınıfların çoklu kalıtımla tanımlanmasına izin verecek kadar işleve sahibim. Aşağıdaki gibi kodlara izin verir. Genel olarak, javascript'te yerel Sınıflandırma tekniklerinden tamamen ayrıldığını göreceksiniz (örneğin, class
anahtar kelimeyi asla görmeyeceksiniz ):
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();
bunun gibi çıktı üretmek için:
human runs with 2 legs.
airplane flies away with 2 wings!
dragon runs with 4 legs.
dragon flies away with 6 wings!
Sınıf tanımları şöyle görünür:
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();
}
}));
makeClass
İşlevi kullanan her bir sınıf tanımının Object
, ebeveyn sınıflarına eşlenen bir ebeveyn sınıfı adlarını kabul ettiğini görebiliriz . Ayrıca Object
, tanımlanmakta olan sınıf için bir kapsayıcı özellikler döndüren bir işlevi de kabul eder . Bu işlevin bir parametresi vardırprotos
üst sınıflardan herhangi biri tarafından tanımlanan herhangi bir özelliğe erişmek için yeterli bilgiyi içeren .
Gereken son parça makeClass
, oldukça fazla iş yapan işlevin kendisidir. İşte kodun geri kalanıyla birlikte. makeClass
Oldukça ağır yorumladım :
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();
makeClass
Fonksiyon aynı zamanda sınıf özelliklerini destekler; bunlar, özellik adlarının önüne $
sembol eklenmesiyle tanımlanır (sonuçta ortaya çıkan son özellik adının $
kaldırılmış olacağını unutmayın ). Bunu aklımızda tutarak, Dragon
Ejderhanın "türünü" modelleyen özel bir sınıf yazabiliriz , burada mevcut Dragon türlerinin listesi, örnekler yerine Sınıfın kendisinde saklanır:
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 });
Çoklu Kalıtımın Zorlukları
Kodu makeClass
yakından takip eden herhangi biri , yukarıdaki kod çalıştığında sessizce meydana gelen oldukça önemli bir istenmeyen fenomeni fark edecektir : bir örnekleme kurucuya RunningFlying
İKİ çağrı ile sonuçlanacaktır Named
!
Bunun nedeni, kalıtım grafiğinin aşağıdaki gibi görünmesidir:
(^^ More Specialized ^^)
RunningFlying
/ \
/ \
Running Flying
\ /
\ /
Named
(vv More Abstract vv)
Bir alt sınıfın kalıtım grafiğinde aynı ebeveyn-sınıfa giden birden çok yol olduğunda, alt sınıfın somutlaştırmaları o üst sınıfın yapıcısını birden çok kez çağıracaktır.
Bununla mücadele etmek önemsiz değil. Basitleştirilmiş sınıf adlarıyla bazı örneklere bakalım. Dersimiz düşünün gerekir A
, en soyut ebeveyn-sınıf, sınıfları B
ve C
, hem devralma A
ve sınıf BC
hangi devralır gelen B
ve C
(ve dolayısıyla kavramsal "çift-devralır" dan 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');
}
}));
BC
Çift çağrılmayı önlemek istiyorsak, A.prototype.init
miras alınan kurucuları doğrudan çağırma tarzını terk etmemiz gerekebilir. Yinelenen aramaların olup olmadığını ve gerçekleşmeden önce kısa devre olup olmadığını kontrol etmek için bir miktar dolaylı yönlendirmeye ihtiyacımız olacak.
Özellikler işlevine sağlanan parametreleri değiştirmeyi düşünebiliriz: miras alınan özellikleri açıklayan ham verilerin yanı sıra protos
, Object
bir örnek yöntemini ana yöntemlerin de çağrılacağı ancak yinelenen çağrıların algılanacağı şekilde çağırmak için bir yardımcı program işlevi de dahil edebiliriz. ve engellendi. Aşağıdakiler için parametreleri nerede oluşturduğumuza bir göz atalım 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 ... */
};
Yukarıdaki şeklindeki değişikliğin tüm amacı, çağırdığımızda makeClass
bizim için ek bir argüman sağladığımızdır . Ayrıca, herhangi bir sınıfta tanımlanan her işlevin , miras alınan yöntemin çağrılmasının bir sonucu olarak zaten çağrılmış olan tüm işlevleri içeren bir ad olan diğerlerinden sonra bir parametre alabileceğinin de farkında olmalıyız :propertiesFn
makeClass
dup
Set
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');
}
}));
Bu yeni stil "Construct A"
, bir örneği BC
başlatıldığında yalnızca bir kez günlüğe kaydedilmesini sağlamayı başarır . Ancak üçüncüsü çok kritik olan üç dezavantaj var :
- Bu kod daha az okunabilir ve bakımı yapılabilir hale geldi.
util.invokeNoDuplicates
İşlevin arkasında pek çok karmaşıklık var ve bu tarzın çoklu çağrıyı nasıl önlediğini düşünmek sezgisel değil ve baş ağrısına neden oluyor. Ayrıca , sınıftaki her işlevdedups
gerçekten tanımlanması gereken sinir bozucu parametreye sahibiz . Ahh.
- Bu kod daha yavaştır - çok sayıda kalıtımla istenen sonuçları elde etmek için biraz daha fazla dolaylı ve hesaplama gerekir. Maalesef bu, çoklu çağrı sorunumuzun herhangi bir çözümünde geçerli olacaktır.
- En önemlisi, kalıtıma dayanan işlevlerin yapısı çok katı hale geldi . Bir alt sınıf
NiftyClass
bir işlevi geçersiz kılarsaniftyFunction
ve util.invokeNoDuplicates(this, 'niftyFunction', ...)
onu yinelenen çağrı olmadan çalıştırmak için kullanırsa , onu tanımlayan her ana sınıfın NiftyClass.prototype.niftyFunction
adını taşıyan işlevi çağırır, niftyFunction
bu sınıflardan herhangi bir dönüş değerini yok sayar ve son olarak özel mantığını gerçekleştirir NiftyClass.prototype.niftyFunction
. Mümkün olan tek yapı budur . Ve NiftyClass
miras alırsa CoolClass
ve GoodClass
bu üst sınıfların her ikisi de niftyFunction
kendi tanımlarını sağlıyorsa NiftyClass.prototype.niftyFunction
, asla (çoklu çağrı yapma riski olmadan):
- A.
NiftyClass
Önce özel mantığını , ardından ebeveyn sınıflarının özel mantığını çalıştırın.
- B. Tüm özel üst düzey mantık tamamlandıktan sonra özel mantığı
NiftyClass
herhangi bir noktada çalıştırın.
- C. Üstünün özel mantığının dönüş değerlerine bağlı olarak koşullu davranır.
- D. Belirli bir ebeveynin uzmanlığını
niftyFunction
tamamen yürütmekten kaçının
Tabii ki, yukarıdaki harflerle yazılmış problemleri, aşağıdaki özel fonksiyonları tanımlayarak çözebiliriz util
:
- A. tanımla
util.invokeNoDuplicatesSubClassLogicFirst(instance, fnName, ...)
- B. tanımla
util.invokeNoDuplicatesSubClassAfterParent(parentName, instance, fnName, ...)
( parentName
Özel mantığının hemen ardından alt sınıfların özel mantığı gelecek olan ebeveynin adı nerede )
- C. tanımla
util.invokeNoDuplicatesCanShortCircuitOnParent(parentName, testFn, instance, fnName, ...)
(Bu durumdatestFn
, adı geçen ebeveyn için özelleşmiş mantığın sonucunu alır ve kısa devrenin olup olmayacağını gösteren parentName
bir true/false
değer döndürür )
- D. tanımla
util.invokeNoDuplicatesBlackListedParents(blackList, instance, fnName, ...)
(Bu durumdablackList
Array
, özel mantığı tamamen atlanması gereken bir ana isim olabilir)
Bu çözümlerin tümü mevcuttur, ancak bu tam bir kargaşa ! Miras alınan bir işlev çağrısının alabileceği her benzersiz yapı için, altında tanımlanan özel bir yönteme ihtiyacımız olacaktır.util
. Ne mutlak bir felaket.
Bunu akılda tutarak, iyi çoklu kalıtım uygulamasının zorluklarını görmeye başlayabiliriz. Tam uygulamamakeClass
Bu yanıtta sağladığım , çoklu çağrı problemini veya çoklu kalıtımla ilgili ortaya çıkan diğer birçok sorunu dikkate almaz.
Bu cevap çok uzuyor. Umarım makeClass
dahil ettiğim uygulama, mükemmel olmasa bile yine de yararlıdır. Ayrıca bu konuyla ilgilenen herkesin daha fazla okurken akılda tutacak daha fazla bağlam kazanmasını umuyorum!