ฉันมีฟังก์ชั่นที่อนุญาตให้กำหนดคลาสด้วยการสืบทอดหลายรายการ อนุญาตให้ใช้รหัสดังต่อไปนี้ โดยรวมแล้วคุณจะสังเกตเห็นการออกจากเทคนิคการจำแนกแบบเนทีฟในจาวาสคริปต์ (เช่นคุณจะไม่เห็นclass
คีย์เวิร์ด):
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();
เพื่อสร้างผลลัพธ์เช่นนี้:
human runs with 2 legs.
airplane flies away with 2 wings!
dragon runs with 4 legs.
dragon flies away with 6 wings!
คำจำกัดความของคลาสมีลักษณะดังนี้:
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
ฟังก์ชันยอมรับObject
ชื่อคลาสพาเรนต์ที่แมปกับคลาสพาเรนต์ นอกจากนี้ยังยอมรับฟังก์ชันที่ส่งคืนObject
คุณสมบัติที่มีสำหรับคลาสที่กำหนด ฟังก์ชันนี้มีพารามิเตอร์protos
ซึ่งมีข้อมูลเพียงพอที่จะเข้าถึงคุณสมบัติใด ๆ ที่กำหนดโดยคลาสพาเรนต์
ชิ้นส่วนสุดท้ายที่จำเป็นคือmakeClass
ฟังก์ชั่นเองซึ่งทำงานได้ไม่น้อย นี่คือรหัสที่เหลือ ฉันแสดงความคิดเห็นmakeClass
ค่อนข้างหนัก:
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
ฟังก์ชั่นนอกจากนี้ยังสนับสนุนคุณสมบัติระดับ; สิ่งเหล่านี้ถูกกำหนดโดยนำหน้าชื่อคุณสมบัติด้วย$
สัญลักษณ์ (โปรดทราบว่าชื่อคุณสมบัติสุดท้ายที่ผลลัพธ์จะ$
ถูกลบออก) ด้วยเหตุนี้เราจึงสามารถเขียนDragon
คลาสพิเศษที่จำลอง "ประเภท" ของมังกรโดยที่รายชื่อประเภทมังกรที่มีอยู่จะถูกเก็บไว้ในคลาสนั้นเองซึ่งต่างจากในอินสแตนซ์:
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 });
ความท้าทายของการสืบทอดหลายอย่าง
ใครก็ตามที่ติดตามรหัสmakeClass
อย่างใกล้ชิดจะสังเกตเห็นปรากฏการณ์ที่ไม่พึงปรารถนาที่ค่อนข้างสำคัญซึ่งเกิดขึ้นอย่างเงียบ ๆ เมื่อโค้ดด้านบนทำงาน: การสร้างอินสแตนซ์ a RunningFlying
จะส่งผลให้มีการเรียกสองครั้งไปยังNamed
สร้าง!
เนื่องจากกราฟการสืบทอดมีลักษณะดังนี้:
(^^ More Specialized ^^)
RunningFlying
/ \
/ \
Running Flying
\ /
\ /
Named
(vv More Abstract vv)
เมื่อมี หลายพา ธ ไปยังคลาสพาเรนต์เดียวกันในกราฟการสืบทอดคลาสย่อยคลาสย่อยอินสแตนซ์ของคลาสย่อยจะเรียกคอนสตรัคเตอร์ระดับพาเรนต์นั้นหลายครั้ง
การต่อสู้แบบนี้ไม่ใช่เรื่องเล็กน้อย ลองดูตัวอย่างที่มีชื่อคลาสแบบง่าย เราจะพิจารณาคลาสA
ซึ่งเป็นคลาสพาเรนต์ที่เป็นนามธรรมที่สุดคลาสB
และคลาสC
ที่สืบทอดมาจากA
คลาสและคลาสBC
ที่สืบทอดมาจากB
และC
(และด้วยเหตุนี้จึงมีคอนเซ็ปต์ "double-inheritits" จาก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
มีการเรียกซ้ำA.prototype.init
เราอาจจำเป็นต้องละทิ้งรูปแบบของการเรียกตัวสร้างที่สืบทอดมาโดยตรง เราจำเป็นต้องมีการกำหนดทิศทางในระดับหนึ่งเพื่อตรวจสอบว่ามีการโทรซ้ำกันหรือไม่และเกิดการลัดวงจรก่อนที่จะเกิดขึ้น
เราสามารถพิจารณาเปลี่ยนพารามิเตอร์ที่จ่ายให้กับคุณสมบัติการทำงาน: ควบคู่ไปกับprotos
การObject
มีข้อมูลดิบอธิบายคุณสมบัติสืบทอดเราอาจจะยังรวมถึงฟังก์ชั่นโปรแกรมสำหรับการเรียกวิธีการเช่นในลักษณะที่ว่าวิธีการปกครองที่เรียกว่ายังเป็น แต่การโทรซ้ำกันจะถูกตรวจพบ และป้องกัน มาดูที่ที่เราสร้างพารามิเตอร์สำหรับ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 ... */
};
วัตถุประสงค์ทั้งของการเปลี่ยนแปลงดังกล่าวข้างต้นจะmakeClass
เป็นเพื่อให้เรามีอาร์กิวเมนต์เพิ่มเติมจ่ายให้กับเราเมื่อเราเรียก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');
}
}));
รูปแบบใหม่นี้ประสบความสำเร็จในการทำให้มั่นใจว่า"Construct A"
จะถูกบันทึกเพียงครั้งเดียวเมื่อBC
เริ่มต้นอินสแตนซ์ แต่มีข้อเสียสามประการซึ่งประการที่สามสำคัญมาก :
- รหัสนี้อ่านและบำรุงรักษาได้น้อยลง ความซับซ้อนมากมายซ่อนอยู่เบื้องหลัง
util.invokeNoDuplicates
ฟังก์ชันนี้และการคิดว่ารูปแบบนี้จะหลีกเลี่ยงการเรียกหลายครั้งได้อย่างไรนั้นไม่ใช่เรื่องง่ายและทำให้ปวดหัว นอกจากนี้เรายังมีdups
พารามิเตอร์ที่น่ารำคาญซึ่งจำเป็นต้องกำหนดไว้ในทุกฟังก์ชันในคลาสทุกฟังก์ชั่นเดียวในชั้นเรียนอุ๊ยตาย
- รหัสนี้ช้ากว่า - ต้องใช้ทิศทางและการคำนวณเพิ่มขึ้นเล็กน้อยเพื่อให้ได้ผลลัพธ์ที่ต้องการด้วยการสืบทอดหลายรายการ แต่น่าเสียดายที่นี้มีแนวโน้มที่จะเป็นกรณีที่มีใด ๆวิธีการแก้ปัญหาหลายภาวนาของเรา
- ส่วนใหญ่อย่างมีนัยสำคัญโครงสร้างของฟังก์ชั่นที่พึ่งพามรดกได้กลายเป็นที่เข้มงวดมาก ถ้าชั้นย่อย
NiftyClass
แทนที่ฟังก์ชั่นniftyFunction
และการใช้util.invokeNoDuplicates(this, 'niftyFunction', ...)
เพื่อให้ทำงานได้โดยไม่ซ้ำกัน-ภาวนาNiftyClass.prototype.niftyFunction
จะเรียกฟังก์ชั่นที่มีชื่อniftyFunction
ของผู้ปกครองชั้นเรียนทุกคนที่กำหนดมันไม่สนใจค่าผลตอบแทนใด ๆ NiftyClass.prototype.niftyFunction
จากชั้นเรียนเหล่านั้นและในที่สุดก็ดำเนินการตรรกะของความเชี่ยวชาญ นี้เป็นเพียงโครงสร้างที่เป็นไปได้ หากNiftyClass
สืบทอดCoolClass
และGoodClass
และคลาสพาเรนต์ทั้งสองนี้ให้niftyFunction
คำจำกัดความของตนเองNiftyClass.prototype.niftyFunction
จะไม่ (โดยไม่เสี่ยงต่อการเรียกใช้หลายครั้ง) จะสามารถ:
- A.เรียกใช้ตรรกะเฉพาะของ
NiftyClass
อันดับแรกจากนั้นจึงใช้ตรรกะเฉพาะของคลาสพาเรนต์
- B.เรียกใช้ลอจิกเฉพาะ
NiftyClass
ที่จุดใดก็ได้นอกเหนือจากหลังจากที่ลอจิกหลักเฉพาะทั้งหมดเสร็จสิ้นแล้ว
- C. ปฏิบัติตามเงื่อนไขโดยขึ้นอยู่กับค่าตอบแทนของตรรกะเฉพาะของผู้ปกครอง
- D.หลีกเลี่ยงการทำงานโดยเฉพาะอย่างยิ่งผู้ปกครองที่เชี่ยวชาญ
niftyFunction
โดยสิ้นเชิง
แน่นอนว่าเราสามารถแก้ปัญหาตัวอักษรข้างต้นได้โดยการกำหนดฟังก์ชันพิเศษภายใต้util
:
- ก.กำหนด
util.invokeNoDuplicatesSubClassLogicFirst(instance, fnName, ...)
- B.กำหนด
util.invokeNoDuplicatesSubClassAfterParent(parentName, instance, fnName, ...)
( parentName
ชื่อของผู้ปกครองที่มีตรรกะเฉพาะจะตามมาทันทีด้วยตรรกะเฉพาะของคลาสเด็ก)
- C. กำหนด
util.invokeNoDuplicatesCanShortCircuitOnParent(parentName, testFn, instance, fnName, ...)
(ในกรณีนี้testFn
จะได้รับผลลัพธ์ของตรรกะเฉพาะสำหรับผู้ปกครองที่ตั้งชื่อparentName
และจะส่งคืนtrue/false
ค่าที่ระบุว่าควรเกิดการลัดวงจรหรือไม่)
- D.กำหนด
util.invokeNoDuplicatesBlackListedParents(blackList, instance, fnName, ...)
(ในกรณีนี้blackList
จะเป็นArray
ชื่อแม่ที่ควรข้ามตรรกะเฉพาะไปเลย)
วิธีแก้ปัญหาเหล่านี้พร้อมใช้งานทั้งหมดแต่นี่คือการทำร้ายร่างกายทั้งหมด ! util
สำหรับโครงสร้างที่ไม่ซ้ำกันทุกคนที่มีสายเรียกฟังก์ชั่นได้รับมรดกสามารถใช้เวลาที่เราจะต้องมีวิธีการเฉพาะที่กำหนดไว้ภายใต้ ช่างเป็นหายนะอย่างแท้จริง
ด้วยเหตุนี้เราจึงสามารถเริ่มเห็นความท้าทายของการใช้มรดกหลายอย่างที่ดี การใช้งานเต็มรูปแบบของmakeClass
ฉันให้ไว้ในคำตอบนี้ไม่ได้พิจารณาถึงปัญหาการเรียกใช้งานหลายครั้งหรือปัญหาอื่น ๆ อีกมากมายที่เกิดขึ้นเกี่ยวกับการสืบทอดหลายรายการ
คำตอบนี้เริ่มยาวมาก ฉันหวังว่าการmakeClass
ใช้งานที่ฉันรวมไว้จะยังคงมีประโยชน์แม้ว่าจะไม่สมบูรณ์แบบก็ตาม ฉันหวังว่าทุกคนที่สนใจในหัวข้อนี้จะได้รับบริบทเพิ่มเติมเพื่อให้ทราบในขณะที่พวกเขาอ่านต่อไป!