Hi, 8Observer8. The reason this seems complicated is because JavaScript doesn’t have classes, strictly speaking, so we have to devise some patterns to make things work.
In Java, a class definition doesn’t consume any memory. It’s not an object yet. It’s a blueprint for eventually creating an object. But in JavaScript, there are no classes, no blueprints. Instead, we’re going to create a concrete, memory-consuming object, and we’re going to treat this object as if it were a prototype – that is, a model from which copies will be spawned.
var PersonPrototype = {
setFirstName: function (firstName) {
this.firstName = firstName;
},
getFirstName: function () {
return this.firstName;
},
};
Here we’ve defined just a plain object with properties. Since this is a real, concrete object, we could start using it if we wanted – PersonPrototype.setFirstName(‘John’) – but we’re not going to do that. We want to keep this object pristine, because we’re going to use it as a model from which we create other objects.
var john = Object.create(PersonPrototype);
john.setFirstName('John');
console.log(john.getFirstName());
Object.create is how we spawn new copies from a prototypical instance. Though, “copy” may be too strong of a word. What actually happens is the object “john” is empty and blank except for one hidden property that points to the object “PersonPrototype”. When we access a property, such as “setFirstName”, JavaScript checks if that property is defined on the “john” object, and if not, then JavaScript follows the prototype link in this case to the “PersonPrototype” object and continues searching for that property. And of course it will indeed find “setFirstName” on the “PersonPrototype” object. This is how JavaScript implements inheritance. Every new “copy” we make of “PersonPrototype” will be blank, but they’ll have that hidden prototype link that allows them to share the behavior that was defined in “PersonPrototype”.
This behavior, all by itself, is already enough to create complex inheritance hierarchies.
var PersonPrototype = {
setFirstName: function (firstName) {
this.firstName = firstName;
},
getFirstName: function () {
return this.firstName;
},
};
var EmployeePrototype = Object.create(PersonPrototype);
EmployeePrototype.setJobTitle = function (jobTitle) {
this.jobTitle = jobTitle;
};
EmployeePrototype.getJobTitle = function () {
return this.jobTitle;
};
var jane = Object.create(EmployeePrototype);
jane.setFirstName('Jane');
jane.setJobTitle('Web Dev');
console.log(jane.getFirstName());
console.log(jane.getJobTitle());
But we’re not done yet. Back when JavaScript was first being created, people thought that prototypal inheritance would seem too foreign to people coming from classical (class-based) inheritance languages, such as Java or C++. So they added some syntactic sugar.
When you define a function – any function – it’s created with a property called “prototype”, whose value is a plain object.
function anyRandomFunction() {
}
// every function gets this prototype property, an object, automatically
console.log(anyRandomFunction.prototype);
The function’s prototype object is meant to serve the purpose as our earlier objects that I suffixed with the word “Prototype”, and the function itself serves as the constructor – that is, the function itself will do any initialization work on all newly created objects. And the way you create objects in this setup is by using the “new” keyword with a function.
// This is the constructor function
// Use it to do any initialization on newly created objects
function Person() {
// Set a default name
this.firstName = 'John'
}
// Add functions (methods) to the function's prototype object
Person.prototype.setFirstName = function (firstName) {
this.firstName = firstName;
};
Person.prototype.getFirstName = function () {
return this.firstName;
};
var harry = new Person();
harry.setFirstName('Harry');
Note that saying new Person() is equivalent to doing:
// Spawn new copy from a prototypical instance
var harry = Object.create(Person.prototype);
// Do any initialization work
// Invoke the constructor, using "harry" as the value for "this"
Person.call(harry);
// Now use it like normal
harry.setFirstName('Harry');
There’s still more to learn than what I’ve described here. JavaScript is flexible enough that there are alternative patterns to achieve similar behavior, and nuances to all of them. It’s complex just enough that there are libraries to make this work easier.