Alright - I've had some time to process everything. I do see valid concerns and ideas with what you've said, which I'll address lower down, but I'm first going to start with some rebuttals to some of your points.
I'm going to go back to comparing against Python. As mentioned previously, classes in python are generally not seen as a bad thing to use, which makes it a good measuring stick to test the strength of different arguments against Javascript class syntax. If that argument also goes against python syntax, then it must not hold that much power, as python classes still stand tall against that argument. It also means the argument isn't intrinsically one about prototypal inheritance, because python doesn't use prototypal inheritance.
- You mentioned that, because of Javascript's prototypal model, a Javascript "class" can return anything, even if it's not an instance of that class. The same is actually true in python as well, and it's not that difficult to do:
class MyClass:
def __new__(*args):
return 2
print(MyClass()) // 2
This is normally seen as a bad practice in both python and javascript, as it goes against what you would expect, but it's easy to do it in both languages.
- You talked about how class fields can't be inspected until an instance is created.
I'll actually defer to C++ for this one. Reading this stackoverflow Q&A it sounds like C++ doesn't provide any way to get the name of class fields either (it doesn't have a reflection mechanism), and getting other kinds of information can only be done through all sorts of hackery nonsense that C++ allows because, well, it's C++.
- You mentioned functions like
Object.keys(instance)
only gets you the keys of your instance fields, it doesn't also provide the keys from the prototype, like a getter.
There isn't really a one-to-one comparison of this behavior in python. But, the core of this problem seems to be the fact that it's important to know which fields end up on the prototype and which ones don't, otherwise, you'll be surprised by certain behaviors. The more general variation of this issue is something that python exhibits.
Take this code snippet for example:
class Stuff:
data = []
obj1 = Stuff()
obj2 = Stuff()
obj1.data.append('x')
print(obj2.data) # ['x']
This is a common pitfall that trips developers up. Unlike in javascript, the python member fields are all static members of the class that are available to be looked up on all instances. This means each instance will share the same member fields. This isn't a problem when you only use primitives, because those are immutable, and reassigning (e.g. via obj.x += 1
) will just cause a new primitive to be placed on the instance that shadows the shared one on the class.
It's important to understand the underlying machinery of how inheritance works in python, and where in the hierarchy chain the different fields go, in order to not get tripped up by seemingly odd behaviors like this.
- A Javascript instance doesn't have a reliable link to a type - the best we've got is
instance.constructor
.
Same with python - absolutely everything is observable, reflectable, and modifiable at runtime (even data that a function has closed over can be accessed and modified! - The one source of privateness we have in javascript). This also means that an instance in python does not have a reliable "type" or link back to whoever created it.
Ok, my rebuttals are now over. You did say that you didn't claim this list to be comprehensive or to be strong enough to warrant your point of view, and so I don't expect these counterarguments to do anything to change your point of view on this matter. But, perhaps there is something to learn from all of this.
I'm really coming to believe that Javascript class syntax being built on top of prototypal inheritance does not give it any more oddities than that of a language like python. In fact, I'm going to go as far as to say that, because of the class syntax, Javascript has classical inheritance, just as much as python or java have it. I know, I know, it's mostly syntax sugar that's built on top of prototypal inheritance. But, what's important is the fact that the class syntax emulates classical inheritance, and that emulation is good enough to make it real. Just like assembly/binary might not natively have functions, but we say C++ has them. In reality, C++ is just providing function syntax that emulates function-like behavior, but at any point, you can dig into the raw assembly and expose the real truth - it's just labels and gotos. Even at runtime you can do fancy memory tricks to get behind the facade and cause the truth to be revealed.
Perhaps as a better example: It can be said that python classes are just "syntax surgar" for their built-in type function (no one says it though...). With the type function, you can dynamically create new classes at runtime, give them a name, an inheritance link, properties, etc. Does that mean python doesn't actually have classes, because it's just syntax surgar for type()? Or is this emulation enough to grant python the badge of "I have classes"?
I'll concede a bit though - the point of this was mostly to show that everything's not so black-and-white, and classes merely being "mostly syntax surgar" isn't actually a great argument for why it might not have a certain feature. So, when people say "Javascript does not have classical inheritance", well, they're correct in some ways, but it's also not completely wrong to say "Javascript has classical inheritance" - we're in a bit of a fuzzy space.
I will acknowledge some other good points I'm seeing in everything you said. In particular, I'm going to prod at a couple of specific quotes that I found to be really golden, and see if they can be mashed together into a conclusion that perhaps we both can agree on.
Perhaps a core problem here isn't the fact that class syntax creates a lot of oddities when built on top of prototypal inheritance (as I just mentioned, I do not believe Javascript classes have any more oddities than python ones), perhaps a core problem is simply the fact that we now conceptually have two forms of inheritance in Javascript: prototypal inheritance, and classical inheritance. Class syntax, in any language, is already a big syntax construct that takes time to learn and understand. And, even though the syntax looks similar from language to language, the specific details of what that syntax does vary widely (like, where a public field value is stored - on the class or on the instance). In Javascript, not only do developers have to learn and understand their version of the class syntax, they also have to learn and understand prototypal inheritance.
So perhaps, from where you stand, you would rather just take a simpler route, ignore Javascript classes, and just use prototypal inheritance. If others libraries give you classes, that's fine, you can still understand and use those classes in terms of prototypes. Prototypes are simple. Classes are complicated. And maybe you even feel that the safety benefits that classes provide, which @ljharb described, are simply not worth the extra complexity and overhead that class syntax provides.
In other words, perhaps the core issue isn't the fact that classes are more confusing and odd because they're built on top of prototypes, rather, in addition to the complexities that any class syntax provides (from any language), Javascript also has prototypes, and that's just too much. There's an easy way to ignore the class-syntax side of Javascript, but it's impossible to completely ignore the prototype side.
Ok, now I'm just putting words in your mouth - but this sounds like a reasonable conclusion to me at least. I still prefer factory functions or class syntax, but I can understand if someone else were to come to a conclusion like this, and would rather have a simpler developing experience than to work with the complexities of dealing with both prototypal inheritance and classical inheritance. But, maybe you completely disagree with everything I said, and still believe that classes built on prototypes just cause all sorts of unwanted oddities
.