클래스를 다중 상속으로 정의 할 수있는 기능이 있습니다. 다음과 같은 코드를 허용합니다. 전반적으로 자바 스크립트에서 네이티브 클래스 기술과 완전히 다른 점을 알 수 있습니다 (예 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
(따라서 개념적으로 "이중 상속"에서 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
A는, 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
모든 특수한 상위 논리가 완료된 후 이외의 시점에서
- 씨. 부모의 특수 로직의 반환 값에 따라 조건부로 행동
- 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
포함 된 구현이 완벽하지는 않지만 여전히 유용하기를 바랍니다 . 또한이 주제에 관심이있는 사람은 더 읽을 때 더 많은 정보를 얻을 수 있기를 바랍니다.