J avaScript is an oddball of a language. It is inspired by Smalltalk and uses a C-like syntax. It puts together aspects of functional, procedural, and object-oriented programming (OCP) models.
It has several, often redundant approaches to find a solution of nearly any conceivable programming difficulty and is not specifically opinionated about which are preferred.
We need to know that arrays aren’t actually arrays; what’s the deal with this, what is a prototype, and what does new basically do?
The difficulty with ES6 Classes
Talks around classes stay frankly alarming and reveal a deep-rooted mix-up of how the language really works:
I listened to these statements, but they don’t bother me. They imply there’s something mistaken with prototypical inheritance; let’s set aside from such arguments.
Before I go any further, let’s exemplify.
The hint here is there isn’t one. These do virtually the same thing; it’s only a query of whether ES6 class syntax was used.
True, the second illustration is more expressive. For that reason only, you might argue that
class is a fine addition to the language. Unluckily, the issue is a little more subtle.
The right answer is that it prints to console:
> MyClass > Overridden in Proto > Overridden in MyClass > Overridden in instance
If you responded incorrectly, you don’t get what class actually is. This is not your fault.
Class , much like
You might have been told that
class keyword doesn’t come with any of the promises that a class is meant to provide. It also determines one of the key differences in the prototype inheritance model: Prototypes are object instances, not types.
Prototypes vs. classes
The key difference between class–and prototype-based inheritance is that a class expresses a type that can be instantiated at runtime, whereas a prototype is itself an object instance.
A child of an ES6 class is another type of definition that extends the parent with new properties and approaches, which in turn can be observed at runtime. A child of a prototype is another object instance that gives the parent any properties that are not implemented on the baby.
new keyword, it designates its prototype as the prototype of the returned object. If that feels a little complicated to you, you’re not alone. It’s a significant part of why prototypes are poorly understood.
To put a real-fine point on that, a child of a prototype isn’t a replica of its prototype, not is it an object with a similar shape as its prototype. The child carries a living reference to the prototype, and any prototype property that doesn’t present on the child is a one-way reference to a property of the same name on the prototype.
Consider the following:
Note: You would almost never write code like this in real life–it’s a terrible practice–but it exhibits the principle concisely.
In the preceding example, while
child.foo was undefined, it referred to
parent.foo . As soon as we described
foo on the
child.foo has the value
‘bar’ , but the
parent.foo hold on to its original value. After we
delete child.foo , it again refers to parent.foo, which leads us to the idea that when we modify the parent’s value,
child.foo refers to the new value.
Let’s take a look at what just took place (for the need of crystal-clear illustration, we’re going to pretend these are
Strings and not string literals, the difference does not matter here):
The chief takeaway is that prototypes don’t define a type; they are themselves
instances and are mutable at runtime, with all that implies and entails.
Our prototypes and class properties that we’ve mentioned above aren’t so much “encapsulated” as “hanging perilously out the window.” We should fix that, but how?
No code illustrations here. The answer is that you can’t.
Do you get what just happened? If not, you don’t understand closures. That’s acceptable, really–they aren’t as intimidating as they are made out to be, they’re extra useful, and you should apply some time to learn about them .
Sorry, this is another trick question. You can do primarily the same thing, but it seems like this:
Let me know if that feels any easier or clearer than in
SecretiveProto . In my personal opinion, it is somewhat worse — it breaks idiomatic implementation of
Let’s find out:
Well… that was inconvenient.
This is not merely about sidestepping the inherent ugliness of inheritance or imposing encapsulation. Think about what else you could do with
leaker that you might not certainly do with a prototype or a class.
For one reason, you can destructure it because you don’t need to worry about the context of
That’s pretty admiring. Besides avoiding
this silliness, it allows us to use objects interchangeably with CommonJS and ES6 modules. It also makes composition a bit easier:
blackHat need no uneasiness about where exfiltrate came from, and
spyFactory doesn’t have to mess around with
Function::bing context juggling or profoundly nested properties.
Mind you, we don’t have to care much about
this in simple synchronous procedural code but leads to all kinds of complications in asynchronous code that is better off avoided.
With a slight thought,
spyFactory could be developed into a convincingly sophisticated espionage tool that could deal with almost all kinds of infiltration targets–or in humble words, a façade .
You could indeed do that with a class too, or rather, a range of classes, all of which inherit from an
abstract class or
Now let’s return to the greater illustration to observe how we would implement it with a factory:
You probably have noticed these factories are turning terser as we go along, but don’t panic–they do the same thing. The training wheels are coming off, folks!
That’s already less boilerplate than either the prototype or the class version of the similar code. Furthermore, it accomplishes the encapsulation of its peculiarities more effectively. Also, it has a lower memory and performance footprint in some cases (it may not appear like it initially, but the JIT compiler is noiselessly working behind the scenes to pare down replication and infer types).
So it’s easier, it’s often faster, and it’s safer to write code like this. Now, the question is that why do we need classes yet again? Oh, of course, reusability. What happens if we want doomed and enthusiastic greeter variants? Well, if we’re applying the
ClassicalGreeting class, we possibly jump straight into dreaming up a class hierarchy. We know we’ll have to parameterize the punctuation, so we’ll do a minor refactoring and add some children:
It’s a feasible approach until someone reaches along and asks for a feature that does’ not fit appropriately into the hierarchy, and the whole thing stops making any sense. Put a pin in that thought while we go for writing the same functionality with factories:
It is not obvious that this code is better, even though it’s a little shorter. In fact, you could claim that it is harder to read, and maybe this an obtuse approach. Couldn’t we simply have an
unhappyGreeterFactory and an
Then your client comes along and says, “I need a new greeter that is unhappy and wants the whole room to know about it!”
If we required to use this eagerly unhappy greeter more than a single time, we could make it easier on ourselves:
There are approaches to this style of composition that function with prototypes or classes. For instance, you could rethink
EnthusiasticGreeting as decorators . It would still take more boilerplate than the functional-style approach used above, but that’s the cost we pay for the safety and encapsulation of real classes.
class usage does a lot of “magic” to paper over these kinds of problems and push classes to behave themselves. Have a look at Polymer’s
Obviously, we can fix some of the issues discussed above with
class neither fixes it nor spares you having to understand prototypes.
I’m not saying to avoid
class entirely. Often, you need inheritance, and
class offers a cleaner syntax for doing that. In general,
class X extends Y is much finer than the old prototype approach. In addition to that, many front-end frameworks favor its use, and you should possibly sidestep writing non-standard code on principle alone. I just do not like where this is going.
In the end, we all give up in frustration and begin reinventing wheels in Rust, Go, Haskell, or who knows what else, and then compiling to Wasm for the web, and new web frameworks and libraries multiply into multilingual infinity.
It really does keep me waking at night.