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, classanahtar 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. makeClassOldukç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();
makeClassFonksiyon 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, DragonEjderhanı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 makeClassyakı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ı Bve C, hem devralma Ave sınıf BChangi devralır gelen Bve 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.initmiras 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, Objectbir ö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 makeClassbizim 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 :propertiesFnmakeClassdupSet
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 BCbaş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
NiftyClassbir 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.niftyFunctionadını taşıyan işlevi çağırır, niftyFunctionbu 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 NiftyClassmiras alırsa CoolClassve GoodClassbu üst sınıfların her ikisi de niftyFunctionkendi 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ığı
NiftyClassherhangi 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ı
niftyFunctiontamamen 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 parentNamebir true/falsedeğer döndürür )
- D. tanımla
util.invokeNoDuplicatesBlackListedParents(blackList, instance, fnName, ...)(Bu durumdablackListArray , ö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 uygulamamakeClassBu 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 makeClassdahil 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!