Let's build a JavaScript class syntax from first principles. For the purpose of this exercise, let's assume that the purpose of the class syntax is to add much-needed sugar to common JavaScript idioms.
Let's start with how JavaScript "classes" work today:
// this is a constructor
Person = function() {
this // `this` is a new instance of Person
}
// define a list of properties that exist on all instances of Person
Person.prototype = {
hello: function(text) {
this // `this` is the current instance of Person
console.log(text)
}
}
Let's add some syntax to JavaScript to represent this idiom:
class Person {
function constructor() {
// `this` is a new instance of Person
}
function hello(text) {
// `this` is the current instance of Person
console.log(text)
}
}
The class is simply the constructor function with the remaining functions assigned to its prototype. If no constructor function is supplied, an empty function is used.
For a dash of sugar, we can allow the word function
to be left out:
class Person {
constructor() {
// `this` is a new instance of Person
}
hello(text) {
// `this` is the current instance of Person
console.log(text)
}
}
Now let's look at inheritance:
// this is a constructor
Man = function() {
Person.apply(this, arguments) // invoke the superclass constructor
this // `this` is a new instance of Man
}
// subclass Person
Man.prototype = Object.create(Person);
// create a new method called fullName
Man.prototype.fullName = function() {
return "Mr. " + this.firstName + ' ' + this.lastName;
}
// subclass `hello` and invoke the superclass
Man.prototype.hello = function(text) {
Person.prototype.hello.call(this, this.fullName() + " says: " + text);
}
Let's add some syntax to JavaScript to represent this idiom:
// same as Man.prototype = Object.create(Person)
class Man extends Person {
// using ES.next rest arguments syntax
constructor(...args) {
super(...args) // same as Person.apply(this, arguments)
}
fullName() {
return "Mr. " + this.firstName + ' ' + this.lastName;
}
hello(text) {
// same as Person.prototype.hello.apply(this, [...args])
super(this.fullName() + " says: " + text);
}
}
The super
keyword simply invokes a function of the same name on the direct superclass of the current object's constructor, passing along any arguments passed.
Finally, people sometimes add properties directly to the prototype:
Man.prototype.salutation = "Mr.";
Let's make that possible inside of the class body:
class Man extends Person {
salutation = "Mr.";
}
This syntax alone would be a vast improvement to existing JavaScript syntax, and would be worthy of inclusion without additional improvements.
That said, let's take the opportunity to make two additional improvements while we're at it.
A common problem with defining properties on a prototype comes when you define a property to be an object, like an Array:
Man.prototype.children = [];
Let's say that instead of evaluating the right hand side of an assignment immediately, and sharing it across all instances, it is evaluated for each new instance:
class Man extends Person {
children = [];
}
new Man().children === new Man().children // false
When creating a new instance, in addition to invoking the class' constructor, we evaluate the right hand side of each declared property and assign it as a property of the new object.
Existing JavaScript libraries that implement classes perform various actions during the process of creating a new class.
Let's allow classes to define a hook that should be called after they are extended.
Person.extended = function(child) {
// child is the subclass of Person
// child.prototype would exist at this point
}
This will allow additional class semantics to be defined by libraries and toolkits.
Let's try adding a very simple class syntax to JavaScript and see where that takes us.