The solution below (class cloning by copying the instance fields and prototype properties) works for me. I am using normal JS (i.e. not Typescript), with JsDoc annotations and VSCode for compile-time type checking.
The solution interface is very simple:
class DerivedBase extends cloneClass(Derived, Base) {}
//To add another superclass:
//class Der1Der2Base extends cloneClass(Derived2, DerivedBase)
let o = new DerivedBase()
The classes involved can be created just like normal ES6 classes.
However the following needs to be done:
- Use isInstanceOf() instead of the builtin operator instanceof.
- Instead of using 'super' to call non-constructor members of the base class, use the function super2() instead.
- Don't write any code in the constructors, instead write it in a class method called 'init()' that is in turn called by the constructor. See the example below.
/* Paste the entire following text into the browser's dev console */
/* Tested on latest version of Chrome, Microsoft Edge and FireFox (Win OS)*/
/* Works with JSDoc in VSCode */
/* Not tested on minified/obfuscated code */
//#region library
const log = console.log
/**
* abbreviation for Object.getPrototypeOf()
* @param {any} o
*/
function proto(o) { return Object.getPrototypeOf(o) }
/** @param {function} fn */
function callIfNonNull(fn) { if (fn != null) { return fn() } }
/**
* @param {boolean} b
* @param {()=>string} [msgFn]
*/
function assert(b, msgFn) {
if (b) { return }
throw new Error('assert failed: ' + ((msgFn == null) ? '' : msgFn()))
}
/** @param {any} o */
function asAny(o) { return o }
/**
* Use this function instead of super.<functionName>
* @param {any} obj
* @param {string} attr the name of the function/getter/setter
* @param {any} cls the class for the current method
* @param {any[]} args arguments to the function/getter/setter
*/
function super2(obj, attr, cls, ...args) {
let nms = clsNms(obj)
assert(nms[0] == nms[1])
const objCls = proto(obj)
const superObj = proto(ancestorNamed(objCls, cls.name))
assert(superObj != obj)
const attrDscr = getOwnOrBasePropDscr(superObj, attr)
if (attrDscr == null) { return null }
let attrVal = attrDscr['value']
const attrGet = attrDscr['get']
const attrSet = attrDscr['set']
if (attrVal == null) {
if (attrGet != null) {
if (attrSet != null) {
assert([0, 1].includes(args.length))
attrVal = ((args.length == 0) ? attrGet : attrSet)
} else {
assert(args.length == 0,
() => 'attr=' + attr + ', args=' + args)
attrVal = attrGet
}
} else if (attrSet != null) {
assert(args.length == 1)
attrVal = attrSet
} else {
assert(false)//no get or set or value!!!!
}
assert(typeof attrVal == 'function')
}
if (typeof attrVal != 'function') { return attrVal }
const boundFn = attrVal.bind(obj)
return boundFn(...args)
}
/**
* Use this function to call own prop instead of overriden prop
* @param {any} obj
* @param {string} attr the name of the function/getter/setter
* @param {any} cls the class for the current method
* @param {any[]} args arguments to the function/getter/setter
*/
function ownProp(obj, attr, cls, ...args) {
let protoObj = ancestorNamed(proto(obj), cls.name)
const attrDscr = Object.getOwnPropertyDescriptor(protoObj, attr)
if (attrDscr == null) {
log(`ownProp(): own property '${attr}' does not exist...`)
return null
}
let attrVal = attrDscr['value']
const attrGet = attrDscr['get']
const attrSet = attrDscr['set']
if (attrVal == null) {
if (attrGet != null) {
if (attrSet != null) {
assert([0, 1].includes(args.length))
attrVal = ((args.length == 0) ? attrGet : attrSet)
} else {
assert(args.length == 0,
() => 'attr=' + attr + ', args=' + args)
attrVal = attrGet
}
} else if (attrSet != null) {
assert(args.length == 1)
attrVal = attrSet
} else {
assert(false)//no get or set or value!!!!
}
assert(typeof attrVal == 'function')
}
if (typeof attrVal != 'function') {
log(`ownProp(): own property '${attr}' not a fn...`)
return attrVal
}
const boundFn = attrVal.bind(obj)
return boundFn(...args)
}
/**
* @param {any} obj
* @param {string} nm
*/
function getOwnOrBasePropDscr(obj, nm) {
let rv = Object.getOwnPropertyDescriptor(obj, nm)
if (rv != null) { return rv }
let protObj = proto(obj)
if (protObj == null) { return null }
return getOwnOrBasePropDscr(protObj, nm)
}
/**
* @param {any} obj
* @param {string} nm
*/
function ancestorNamed(obj, nm) {
const ancs = ancestors(obj)
for (const e of ancs) {
if ((e.name || e.constructor.name) == nm) { return e }
}
}
/**
* @template typeOfDerivedCls
* @template typeOfBaseCls
* @param {typeOfDerivedCls} derivedCls
* @param {typeOfBaseCls} baseCls
* @returns {typeOfDerivedCls & typeOfBaseCls}
*/
function cloneClass(derivedCls, baseCls) {
const derClsNm = derivedCls['name'], baseClsNm = baseCls['name']
const gbl = globalThis
//prevent unwanted cloning and circular inheritance:
if (isInstanceOf(baseCls, asAny(derivedCls))) { return asAny(baseCls) }
if (isInstanceOf(derivedCls, asAny(baseCls))) { return asAny(derivedCls) }
//Object does not derive from anything; it is the other way round:
if (derClsNm == 'Object') { return cloneClass(baseCls, derivedCls) }
//use cached cloned classes if available
if (gbl.clonedClasses == null) { gbl.clonedClasses = {} }
const k = derClsNm + '_' + baseClsNm, kVal = gbl.clonedClasses[k]
if (kVal != null) { return kVal }
//clone the base class of the derived class (cloning occurs only if needed)
let derBase = cloneClass(proto(derivedCls), baseCls)
//clone the derived class
const Clone = class Clone extends derBase {
/** @param {any[]} args */
constructor(...args) {
super(...args)
ownProp(this, 'init', Clone, ...args)
}
}
//clone the properties of the derived class
Object.getOwnPropertyNames(derivedCls['prototype'])
.filter(prop => prop != 'constructor')
.forEach(prop => {
const valToSet =
Object.getOwnPropertyDescriptor(derivedCls['prototype'], prop)
if (typeof valToSet == 'undefined') { return }
Object.defineProperty(Clone.prototype, prop, valToSet)
})
//set the name of the cloned class to the same name as its source class:
Object.defineProperty(Clone, 'name', { value: derClsNm, writable: true })
//cache the cloned class created
gbl.clonedClasses[k] = Clone
log('Created a cloned class with id ' + k + '...')
return asAny(Clone)
}
/**
* don't use instanceof throughout your application, use this fn instead
* @param {any} obj
* @param {Function} cls
*/
function isInstanceOf(obj, cls) {
if (obj instanceof cls) { return true }
return clsNms(obj).includes(cls.name)
}
/** @param {any} obj */
function clsNms(obj) {
return ancestors(obj).map(/** @param {any} e */ e =>
e.name || e.constructor.name)
}
/**
* From: https://gist.github.com/ceving/2fa45caa47858ff7c639147542d71f9f
* Returns the list of ancestor classes.
*
* Example:
* ancestors(HTMLElement).map(e => e.name || e.constructor.name)
* => ["HTMLElement", "Element", "Node", "EventTarget", "Function", "Object"]
* @param {any} anyclass
*/
function ancestors(anyclass) {
if (anyclass == null) { return [] }
return [anyclass, ...(ancestors(proto(anyclass)))]
}
//#endregion library
//#region testData
class Base extends Object {
/** @param {any[]} args */
constructor(...args) {//preferably accept any input
super(...args)
ownProp(this, 'init', Base, ...args)
}
/** @param {any[]} _args */
init(..._args) {
log('Executing init() of class Base...')
//TODO: add code here to get the args as a named dictionary
//OR, follow a practice of parameterless constructors and
//initial-value-getting methods for class field intialization
/** example data field of the base class */
this.baseClsFld = 'baseClsFldVal'
}
m1() { log('Executed base class method m1') }
b1() { log('Executed base class method b1') }
get baseProp() { return 'basePropVal' }
}
class Derived extends Object {//extend Object to allow use of 'super'
/** @param {any[]} args */
constructor(...args) {//convention: accept any input
super(...args)
ownProp(this, 'init', Derived, ...args)
}
/** @param {any[]} _args */
init(..._args) {
log('Executing init() of class Derived...')
this.derClsFld = 'derclsFldVal'
}
m1() {
const log = /** @param {any[]} args */(...args) =>
console.log('Derived::m1(): ', ...args)
log(`super['m1']: `, super['m1'])
super2(this, 'm1', Derived)
log(`super['baseProp']`, super['baseProp'])
log(`super2(this, 'baseProp', Derived)`,
super2(this, 'baseProp', Derived))
log(`super2(this, 'nonExistentBaseProp', Derived)`,
super2(this, 'nonExistentBaseProp', Derived))
}
m2() {
log('Executed derived class method 2')
}
}
class DerivedBase extends cloneClass(Derived, Base) {
/** @param {any[]} args */
constructor(...args) {
super(...args)
ownProp(this, 'init', DerivedBase, ...args)
}
/** @param {any[]} _args */
init(..._args) {
log('Executing init() of class DerivedBase...')
}
}
log('Before defining o (object of DerivedBase)...')
let o = new DerivedBase()
log('After defining o (object of DerivedBase)...')
class Derived2 extends Base {
/** @param {any} args */
constructor(...args) {
//convention/best practice: use passthrough constructors for the classes
//you write
super(...args)
ownProp(this, 'init', Derived2, ...args)
}
/**
* @param {any[]} _args
*/
init(..._args) {
log('Executing init() of class Derived2...')
}
derived2func() { log('Executed Derived2::derived2func()') }
}
class Der1Der2Base extends cloneClass(Derived2, DerivedBase) {
/** @param {any} args */
constructor(...args) {
//convention/best practice: use passthrough constructors for the classes
//you write
super(...args)
ownProp(this, 'init', Der1Der2Base, ...args)
}
/** @param {any[]} _args */
init(..._args) {
log('Executing original ctor of class Der1Der2Base...')
}
}
log('Before defining o2...')
const o2 = new Der1Der2Base()
log('After defining o2...')
class NotInInheritanceChain { }
//#endregion testData
log('Testing fields...')
log('o.derClsFld:', o.derClsFld)
log('o.baseClsFld:', o.baseClsFld)
//o.f3 JSDoc gives error in VSCode
log('Test method calls')
o.b1()
o.m1()
o.m2()
//o.m3() //JSDoc gives error in VSCode
log('Test object o2')
o2.b1()
o2.m1()
o2.derived2func()
//don't use instanceof throughout your application, use this fn instead
log('isInstanceOf(o,DerivedBase)', isInstanceOf(o, DerivedBase))
log('isInstanceOf(o,Derived)', isInstanceOf(o, Derived))
log('isInstanceOf(o,Base)', isInstanceOf(o, Base))
log('isInstanceOf(o,NotInInheritanceChain)',
isInstanceOf(o, NotInInheritanceChain))
A note of caution: the JSDoc intersection operator & may not always work. In that case some other solution may need to be used. For example, a separate interface class may need to be defined that will 'manually' combine the 2 classes. This interface class can extend from one of the classes and the other class's interface can be automatically implemented using VsCode quick fix option.