I first wrote this stackoverflow answer in 2015. Obviously things has changed quite a bit, but still think there are much misdirections in JavaScript to address.
This article, as its title would suggest, will be contentious. But please, I am not about to say that we shouldn't use class
and new
. But to make that little dent, catch your attention, and hopefully we can all have some discussions over it.
Mainly is to explore, through a simple syntax, that Javascript is inherently classless, and its powerful prototypial nature obscured by class
and new
.
But, on balance, you have much to gain, and nothing to lose using ES6 classes (provided one writes it readably).
The point at the end of the day, please think of readability. The closer a language looks like to a human language, the better.
World without the "new" keyword.
And simpler "prose-like" syntax with Object.create().
First off, and factually, Javascript is a prototypal language, not class-based. The class
keyword is in fact is just prototypial under the hood. Indulge me, and have a look at its true nature expressed in the simple prototypial form below, which you may come to see that is very simple, prose-like, yet powerful. I also will not use the prototype
property, because I also find it rather unnecessary and complicated.
TLDR;
const Person = {
firstName: 'Anonymous',
lastName: 'Anonymous',
type: 'human',
name() { return `${this.firstName} ${this.lastName}`},
greet() {
console.log(`Hi, I am ${this.name()}.`)
}
}
const jack = Object.create(Person) // jack is a person
jack.firstName = 'Jack' // and has a name 'Jack'
jack.greet() // outputs "Hi, I am Jack Anonymous."
This absolves the sometimes convoluted constructor pattern. A new object inherits from the old one, but is able to have its own properties. If we attempt to obtain a member from the new object (#greet()
) which the new object jack
lacks, the old object Person
will supply the member.
You don't need constructors, no new
instantiation (read why you shouldn't use new
), no super
, no self-made __construct
, no prototype
assignments. You simply create Objects and then extend or morph them.
This pattern also offers immutability (partial or full), and getters/setters.
TypeScript Equivalent
The TypeScript equivalent requires declaration of an interface:
interface Person {
firstName: string,
lastName: string,
name: Function,
greet: Function
}
const Person = {
firstName: 'Anonymous',
lastName: 'Anonymous',
name(): string { return `${this.firstName} ${this.lastName}`},
greet(): void {
console.log(`Hi, I am ${this.name()}.`)
}
}
const jack: Person = Object.create(Person)
Creating an descendant/copy of Person
Note: The correct terms are
prototypes
, and theirdescendants/copies
. There are noclasses
, and no need forinstances
.
const Skywalker = Object.create(Person)
Skywalker.lastName = 'Skywalker'
const anakin = Object.create(Skywalker)
anakin.firstName = 'Anakin'
anakin.gender = 'male' // you can attach new properties.
anakin.greet() // 'Hi, my name is Anakin Skywalker.'
Let's look at the prototype chain:
/* Person --> Skywalker --> anakin */
Person.isPrototypeOf(Skywalker) // outputs true
Person.isPrototypeOf(anakin) // outputs true
Skywalker.isPrototypeOf(anakin) // outputs true
If you feel less safe throwing away the constructors in-lieu of direct assignments, fair point. One common way is to attach a #create
method which you are read more about below.
Branching the Person
prototype to Robot
Say when we want to branch and morph:
// create a `Robot` prototype by extending the `Person` prototype
const Robot = Object.create(Person)
Robot.type = 'robot'
Robot.machineGreet = function() { console.log(10101) }
// `Robot` doesn't affect `Person` prototype and its descendants
anakin.machineGreet() // error
And the prototype chain looks like:
/*
Person ----> Skywalker --> anakin
|
|--> Robot
*/
Person.isPrototypeOf(Robot) // outputs true
Robot.isPrototypeOf(Skywalker) // outputs false
...And Mixins -- Because.. is Darth Vader a human or robot?
const darthVader = Object.create(anakin)
// for brevity, skipped property assignments
// you get the point by now.
Object.assign(darthVader, Robot)
// gets both #Person.greet and #Robot.machineGreet
darthVader.greet() // "Hi, my name is Darth Vader..."
darthVader.machineGreet() // 10101
Along with other odd things:
console.log(darthVader.type) // outputs "robot".
Robot.isPrototypeOf(darthVader) // returns false.
Person.isPrototypeOf(darthVader) // returns true.
Which elegantly reflects the "real-life" subjectivity:
"He's more machine now than man, twisted and evil." - Obi-Wan Kenobi
"I know there is good in you." - Luke Skywalker
In TypeScript you would also need to extend the Person
interface:
interface Robot extends Person {
machineGreet: Function
}
Conclusion
I have no qualms with people thinking that class
and new
are good for Javascript because it makes the language familiar and also provides good features. I use those myself. The issue I have is with people extending on the aforementioned basis, to conclude that class
and new
is just a semantics issue. It just isn't.
It also gives rise to tendencies to write the simple language of Javascript into classical styles that can be convoluted. Instead, perhaps we should embrace:
-
class
andnew
are great syntactic sugar to make the language easier to understand for programmers with class languages background, and perhaps allows a structure for translating other other languages to Javascript. - But under the hood, Javascript is prototypial.
- And after we have gotten our head around Javascript, to explore it's prototypial and more powerful nature.
Perhaps in parallel, it should allow for a proto
and create
keyword that works the same with all the ES6 classes good stuff to absolve the misdirection.
Finally, whichever it is, I hoped to express through this article that the simple and prose-like syntax has been there all along, and it had all the features we needed. But it never caught on. ES6 classes are in general a great addition, less my qualm with it being "misleading". Other than that, whatever syntax you wish to use, please consider readability.
Further reading
Commonly attached #create
method
Using the Skywalker
example, suppose you want to provide the convenience that constructors brings without the complication:
Skywalker.create = function(firstName, gender) {
let skywalker = Object.create(Skywalker)
Object.assign(skywalker, {
firstName,
gender,
lastName: 'Skywalker'
})
return skywalker
}
const anakin = Skywalker.create('Anakin', 'male')
On #Object.defineProperty
For free getters and setters, or extra configuration, you can use Object.create()'s second argument a.k.a propertiesObject. It is also available in #Object.defineProperty, and #Object.defineProperties.
To illustrate its usefulness, suppose we want all Robot
to be strictly made of metal (via writable: false
), and standardise powerConsumption
values (via getters and setters).
const Robot = Object.create(Person, {
// define your property attributes
madeOf: {
value: "metal",
writable: false,
configurable: false,
enumerable: true
},
// getters and setters
powerConsumption: {
get() { return this._powerConsumption },
set(value) {
if (value.indexOf('MWh')) {
this._powerConsumption = value.replace('M', ',000k')
return
}
this._powerConsumption = value
throw Error('Power consumption format not recognised.')
}
}
})
const newRobot = Object.create(Robot)
newRobot.powerConsumption = '5MWh'
console.log(newRobot.powerConsumption) // outputs 5,000kWh
And all prototypes of Robot
cannot be madeOf
something else:
const polymerRobot = Object.create(Robot)
polymerRobot.madeOf = 'polymer'
console.log(polymerRobot.madeOf) // outputs 'metal'
Top comments (7)
This article and others like it in the JavaScript community remind me of how Java and C# people rejected JavaScript. Some rejected it for 10 years or more, until the ubiquitous nature of JavaScript forced everyone to learn it. Today's JavaScript is radically different from the first release. So many improvements have been made that almost everyone likes it now.
The JavaScript Prototype's Compositional Signature
This is the signature of a compositional pattern. It shows us there are three composites, a Person, a Prototype and a property. Composition is always good. But this particular code is busy, a full 32 chars. just to get to what the function does. I don't and never have liked the readability factor here.
Typescript Vaporizes Prototype Syntax!
... by automatically compiling all properties to prototypes. We never need to write the prototype syntax again!
Everything else in a Typescript compile are functions, including the "Class" construct if we target ESM5.
This means : free getter, setters, no use of Object.create, no diving to the obscured prototype layer, fantastic readability. If we don't like the 'new' or 'constructor' syntax, then we just skip them like this:
No need for interface definitions because a class is an interface with the added ability to initialize values! Simple, terse, clean and easy. Super readable!
In all honesty, the JavaScript Community's resistance to new approved standards within their own language is understandable. They are blinded to their first love as it was. The only problem now is that the first love is no longer there.
Nicely put, I can't agree with you more. The greatest relief in fact, if any, about the ES6 classes syntax, or the TypeScript syntax, is thankfully -- as you rightfully pointed out -- that you can still write simply and readably. Albeit it also can be turned into something really hardcore that looks half-intellectual and half-mangled. I really don't like the people who write such codes thinking that it's your fault you won't understand it.
So I guess I had two main points which is that readability must always be preserved. And yes related to the first love, still can't let go 😂 -- that it should have been
proto
instead ofclass
, andcreate
instead ofnew
.Wonderful comment, thanks.
Btw, your code gives the error of
Cannot invoke an object which is possibly 'undefined'.
onobj.fullName()
. Any idea how to fix that?Also, why do you need to wrap it in
#UsingThePersonClass()
.The wrapping was just to show how to use the Person class.
This right here is, I think, my biggest pet-peeve with Angular and Typescript in general. We don't need
new
orclass
orsuper
or any of that OOP stuff, JS is already a quite expressive and flexible language when used right.Sadly, most people (myself included) come from a backend background. And some don't even try to learn & understand the language in the way it is.
Instead, they bring all the old customs and ways of thinking of languages like Java & C# and complain that JS is "weird" and "difficult to learn"
One of my perspectives spot on. There's the counter argument that some of these
new
,class
orsuper
helps to reduce code errors. Yes to a certain extent, but code that is difficult to read can lead to errors too, for one can make mistake if one is unable to fully understand. Ultimately whether plain old object literal/object.create syntax or ES6 classes, the idea is to write things simply.And yes, I think Angular is sometimes over engineered.