Created
August 18, 2024 08:18
-
-
Save dfkaye/09538d6be55329c76831cdd78a3a2859 to your computer and use it in GitHub Desktop.
simulate multiple inheritance in javascript
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 5 Aug 2024 | |
// multiple inheritance | |
// "inspired" by tweet, 1 August 2024, from Colin McDonnell (and goaded by some | |
// benighted replies) at https://x.com/colinhacks/status/1819138095104905689 | |
// Challenge: Implement merge() to handle multiple inheritance, as per example: | |
// ``` | |
// class A {...} | |
// class B {...} | |
// class C extends merge(A, B) {...} | |
// ``` | |
// Our merge() implementation returns a class that our subject class extends. | |
// That class mixes in each constructor's prototype properties and constructed | |
// instance properties. We can get the inheritance to work from either classes | |
// or functions (really just their constructors) and create super properties. | |
// But to inherit a property by name, we are limited to the last one defined on | |
// a class or constructor's prototype (i.e., last one wins). And the instanceof | |
// check fails against any base class because objects cannot inherit from more | |
// than one prototype. ("Fails" is a strong word here. Once we understood the | |
// limitation that we cannot set more than one prototype on an object, we chose | |
// not set the prototype at all). | |
// Not tested at 3 levels depth or more. | |
// Tests compare super properties with and without extending merge(), and verify | |
// that methods on classes with frozen prototypes cannot be reassigned. | |
function merge(...arguments) { | |
// Functions (and classes) only. | |
var constructors = arguments.filter(A => typeof A == "function"); | |
// F is returned by merge() to be inherited by the user of extends. F could | |
// just as well be a function rather than a class with a constructor. | |
class F { constructor(...params) { | |
var pos = 0; | |
var i = 0; | |
for (var C of constructors) { | |
// Walk through each constructor to be merged in call order, passing | |
// arguments for each based on position (index plus arguments length). | |
var from = i + pos; | |
var to = from + C.length; | |
var args = params.slice(from, to); | |
// Assign instance properties from the constructed object to the | |
// inheriting instance (this). | |
Object.assign(this, new C(...args)); | |
// Move the next args offset. | |
pos = args.length - 1; | |
// We're in a for-of loop, so increment the index with our bare hands. | |
i += 1; | |
} | |
}} | |
// Extend class F's prototype with prototype properties from each constructor. | |
// For any name on F's prototype, only the last prototype containing that name | |
// is assigned (i.e., previous values are overwritten and the last one wins). | |
constructors.forEach(function (C) { | |
for (var n of Object.getOwnPropertyNames(C.prototype)) { | |
var p = C.prototype[n]; | |
if (p !== C) { | |
F.prototype[n] = p; | |
} | |
} | |
}); | |
return F; | |
} | |
//////////////////////////////////////////////////////////////////////////////// | |
// | |
// test it out | |
// | |
//////////////////////////////////////////////////////////////////////////////// | |
;(function test(name = "extends with merge") { | |
console.group(name); | |
class A { | |
constructor(a) { | |
this.a = a; | |
this.hello([...arguments].join(" ")); | |
} | |
hello(msg) { | |
console.log(this.constructor.name, msg); | |
console.log(this.constructor.name, this); | |
} | |
} | |
// class B { | |
// constructor(c) { | |
// this.b = b; | |
// this.c = c; | |
// this.hello([...arguments].join(" ")); | |
// } | |
// hello(msg) { | |
// //console.log(this.constructor.name, msg); | |
// //console.log(this); | |
// } | |
// } | |
function B(b, c) { | |
this.b = b; | |
this.c = c; | |
this.hello([...arguments].join(" ")); | |
} | |
B.prototype.hello = function(msg) { | |
console.warn(this.constructor.name, msg); | |
console.log(this.constructor.name, this); | |
} | |
var D = { | |
constructor() { | |
throw new Error("should not merge prototype for D"); | |
} | |
}; | |
// Our test subject. Note that the inheriting class should accept all params | |
// for the merged classes in call order. For example, where A accepts a and B | |
// accepts b and c, the call to new C should pass (a, b, c) followed by any | |
// C-specific arguments. The inheriting class should then pass all arguments | |
// to super(), as super(...arguments). | |
class C extends merge(A, B, D) { | |
constructor( /* a, b, c, d */ ) { | |
super(...arguments); | |
var name = [...arguments].at(-1); | |
this[name] = name; | |
this.hello("from C"); | |
} | |
hello(msg) { | |
super.hello(msg); | |
console.log(this.constructor.name, msg); | |
console.log(this.constructor.name, this); | |
} | |
test(property, value) { | |
return arguments.length > 1 | |
? this[property] = value | |
: this[property]; | |
} | |
} | |
// Our test instance to assert against. | |
var c = new C("test", "multiple", "inheritance", "nonce"); | |
// A second instance to verify that inherited constructors and prototypes are | |
// *not* polluted... | |
(new C("and", "another", "inheritance", "test")); | |
// Instances of the inherited constructors to verify they don't pollute each | |
// other - it could happen, it could happen... | |
(new A("hello", "dog")); | |
(new B("well", "dude", "whatever")); | |
// These will be false as we don't reset the prototype chain. | |
console.assert( !(c instanceof A), "c should not be instance of A" ); | |
console.assert( !(c instanceof B), "c should not be instance of B" ); | |
// Let's go... | |
console.assert(c.constructor === C, "c constructor should be C"); | |
console.assert(c instanceof C, "c should be an instance of C"); | |
console.assert(c.a === "test", "c.a should be test"); | |
console.assert(c.b === "multiple", "c.b should be multiple"); | |
console.assert(c.c === "inheritance", "c.c should be inheritance"); | |
console.assert(c.nonce === "nonce", "c.nonce should be nonce"); | |
~(function (name = "mutation tests") { | |
console.group(name); | |
c.b = 5; | |
console.assert(c.test("b") === 5, "should return c.b"); | |
console.assert(c.test("b", 88) === 88, "should set c.b to 88"); | |
console.assert(c.b === 88, "c.b should be 88"); | |
console.groupEnd(name); | |
})(); | |
console.groupEnd(name); | |
})(); | |
;(function (name = "super properties with extends") { | |
console.group(name); | |
class A { | |
constructor (a) { | |
this.a = a; | |
} | |
c = "inherited but not on the super" | |
} | |
A.prototype.b = "default..."; | |
class B extends A { constructor (a, b) { | |
super(a); | |
this.b = b; | |
console.assert(this.a === a, "this.a should equal arg a"); | |
console.assert(this.b === b, "this.b should equal arg b"); | |
console.assert( | |
this.c === "inherited but not on the super", | |
"should inherit c from A" | |
); | |
console.assert(super.a === undefined, "a should not be visible on super"); | |
console.assert(super.b === "default...", "b should be visible on super"); | |
console.assert(super.c === undefined, "c should not be visible on super"); | |
}} | |
new B(10, 20); | |
console.groupEnd(name); | |
})(); | |
;(function (name = "super properties with merge") { | |
console.group(name); | |
class A { | |
constructor (a) { | |
this.a = a; | |
} | |
c = "inherited but not on the super" | |
} | |
A.prototype.b = "visible on super"; | |
class B extends merge(A) { constructor (a, b) { | |
super(a); | |
this.b = b; | |
console.assert(this.a === a, "this.a should equal arg a"); | |
console.assert(this.b === b, "this.b should equal arg b"); | |
console.assert( | |
this.c === "inherited but not on the super", | |
"should inherit c from A" | |
); | |
console.assert(super.a === undefined, "a should not be visible on super"); | |
console.assert( | |
super.b === "visible on super", | |
"b should be visible on super" | |
); | |
console.assert(super.c === undefined, "c should not be visible on super"); | |
}} | |
new B(10, 20); | |
console.groupEnd(name); | |
})(); | |
;(function (name = "frozen prototype") { | |
console.group(name); | |
class A { | |
constructor (a) { | |
this.aa = a; | |
} | |
} | |
class B extends A { | |
constructor(a, b, c) { | |
super(...arguments); | |
this.B = b; | |
this.C = c; | |
} | |
// interpreter will not complain about B | |
B = 5 | |
// interpreter will complain about C being read-only | |
C() {} | |
} | |
/* test it out */ | |
Object.freeze(B.prototype); | |
var error; | |
try { new B('ba', 'bb', 'expando') } | |
catch (e) { error = e.message; } | |
finally { | |
console.assert(error.includes("C") && /read-only|(read only)/.test(error)); | |
} | |
console.groupEnd(name); | |
})(); | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment