Skip to content

Instantly share code, notes, and snippets.

@dfkaye
Created August 18, 2024 08:18
Show Gist options
  • Save dfkaye/09538d6be55329c76831cdd78a3a2859 to your computer and use it in GitHub Desktop.
Save dfkaye/09538d6be55329c76831cdd78a3a2859 to your computer and use it in GitHub Desktop.
simulate multiple inheritance in javascript
// 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