This answer is intentionally a detailed "I don't think you can", in an effort to help others who are trying to do the same thing understand why I concluded that it's not possible, at least not without eval()
and maybe not even then. I have a fairly deep understanding of Javascript's prototypical class implementation, but there are many who have deeper knowledge than me.
My Requirements
I came here with the question stated in this post, and also ended up settling for a class factory. My requirement was that the cloned class is identical to one obtained with a class factory. Specifically, I want a function such that this:
class Parent {}
class Child extends Parent {}
const Sibling = cloneClass(Child)
results in the exact same state as this:
class Parent {}
function classFactory() {
return class Child extends Parent {}
}
const Child = classFactory()
const Sibling = classFactory()
Things that are thus dealbreakers for my purposes:
- A
Sibling
that is an instance of or prototypically descended from Child
(its first prototype should be Parent
)
- A
Sibling
that shares a prototype or function definitions with Child
For my needs, it is useful if Sibling.name == Child.name == 'Child'
, which is the case in the class-factory setup.
Technically to be exactly the same there are more requirements (Sibling
's methods can't be prototypically descended from Child
's methods, for instance), but I think that is moot, for reasons that will soon become apparent.
The sticky bits
The thing that makes this impossible by my reckoning is actually quite simple: it requires cloning functions, and you can't do that. There are a couple questions about it here, which have various solutions that work in some cases but are not actually cloning, and don't fulfill my requirements. For what it's worth, functions are on lodash's list of "uncloneable values" as well.
This is relevant for class methods, but more crucially, it is relevant because under the hood, classes are functions. In a very literal sense, a class is its constructor function. Thus, even if you could deal with class methods that are prototypically descended from Child
's methods, I think you can't get around the fact that that also means that the Sibling
class itself - which is a function - will also have to be prototypically descended from Child
.
Edit: See below, if Child
doesn't have a constructor function, then you may be able to skirt around this requirement.
Other thoughts/caveats
I'm happy to be proven wrong here, or have my understanding corrected, but I think that is the central blocker here: classes are functions, and you can't clone a function.
One route I haven't pursued, because it is darker magic than I am willing to delve into, is using the Function()
constructor to almost-but-not-quite-eval yourself into a truly "cloned" function. I am not sure if this is possible, and I am not knowledgeable enough of the implications of doing so to try.
Requirements Demo
If you want to have a go at it, I made a snippet that has a few tests that assert my requirements - if you can get my requirements passing with a clone function, do let me know!
Edit: @Bergi offered up a solution that I've included in the snippet below. It does seem to do the job! I believe it gets around the problem of cloning the constructor by...not doing so, since in my case the child classes don't have their own constructor. Thus an empty function (with everything else tacked on) is indeed equivalent to a clone. It also comes with all the standard disclaimers around using setPrototype.
'use strict'
let hadWarning = false
const getProto = Object.getPrototypeOf
class Parent {}
function classFactory () {
return class Child extends Parent {}
}
function factoryTest () {
const Child = classFactory()
const Sibling = classFactory()
runTest(Child, Sibling, 'classFactory')
}
/* Adapted from @Bergi */
function cloneClass (Target, Source) {
return Object.defineProperties(
Object.setPrototypeOf(
Target,
Object.getPrototypeOf(Source)
),
{
...Object.getOwnPropertyDescriptors(Source),
prototype: {
value: Object.create(
Object.getPrototypeOf(Source.prototype),
Object.getOwnPropertyDescriptors(Source.prototype)
)
}
}
)
}
function berghiTest () {
class Child extends Parent {}
const Sibling = cloneClass(function Sibling () {}, Child)
runTest(Child, Sibling, 'Bergi\'s clone')
}
factoryTest()
berghiTest()
/* Assertion support */
function fail (message, warn) {
if (warn) {
hadWarning = true
console.warn(`Warning: ${message}`)
} else {
const stack = new Error().stack.split('\n')
throw new Error(`${message} ${stack[3].trim()}`)
}
}
function assertEqual (expected, actual, warn) {
if (expected !== actual) {
fail(`Expected ${actual} to equal ${expected}`, warn)
}
}
function assertNotEqual (expected, actual, warn) {
if (expected === actual) {
fail(`Expected ${actual} to not equal ${expected}`, warn)
}
}
function runTest (Child, Sibling, testName) {
Child.classTag = 'Child'
Sibling.classTag = 'Sibling'
hadWarning = false
assertEqual(Child.name, 'Child')
assertEqual(Sibling.name, Child.name, true) // Maybe not a hard requirement, but nice
assertEqual(Child.classTag, 'Child')
assertEqual(Sibling.classTag, 'Sibling')
assertEqual(getProto(Child).name, 'Parent')
assertEqual(getProto(Sibling).name, 'Parent')
assertEqual(getProto(Child), Parent)
assertEqual(getProto(Sibling), Parent)
assertNotEqual(Child.prototype, Sibling.prototype)
assertEqual(getProto(Child.prototype), Parent.prototype)
assertEqual(getProto(Sibling.prototype), Parent.prototype)
const child = new Child()
const sibling = new Sibling()
assertEqual(sibling instanceof Child, false)
assertEqual(child instanceof Parent, true)
assertEqual(sibling instanceof Parent, true)
if (hadWarning) {
console.log(`${testName} passed (with warnings)`)
} else {
console.log(`${testName} passed!`)
}
}