Prototypal Inheritance
This way of doing inheritance is taken from JS Objects: Deconstruct
ion.
Also check the slides of The four layers of JavaScript OOP.
Basics
We define a "class" Person
just as an object.
var Person = {
sayName: function () {
return 'My name is ' + this.name;
}
};
We create an instance from it, using Object.create
.
var jane = Object.create(Person);
This created an empty object with Person
as its prototype, so jane
s prototype chain now looks like this:
jane -------> Person
sayName
Now, we give our instance a name
.
jane.name = 'Jane';
The string 'Jane'
is not being stored in Person
, but in jane
itself. Assignments to objects never go to their prototype. jane
s prototype chain will now look like this:
jane -------> Person
name sayName
Let's test what we wrote.
jane.sayName(); // -> 'My name is Jane'
When we access a field (e.g. name
or sayName
) of an object, JavaScript will walk up its prototype chain, trying to find the field. First, it will look inside the jane
object. Because sayName
is not in jane
, it will move up and look for the entry in Person
.
Note that, even through sayName
is being taken from Person
, we call it on jane
. this
in the code of sayName
then refers to jane
.
Let's create another instance from Person
and call it John.
var john = Object.create(Person);
john.name = 'John';
john
s prototype chain will now look like the one of jane
:
john -------> Person
name sayName
Imagine we modify Person
(instead of the instances jane
and john
):
Person.sayName = function () {
return 'I am ' + this.name;
};
Because Person
is in the prototype chain of each of our instances jane
and john
, our modification also changes their behavior. Unlike with (real) class-based inheritance, using prototypal inheritance allows for modifying "superclasses" during runtime.
john.sayName(); // -> 'I am John'
Subclasses
To create a subclass Student
of Person
, we just create a new object with Person
in its prototype chain and extend it by a function learn
.
var Student = Object.create(Person);
Student.learn = function (skill) {
return 'Learning all about ' + skill;
};
Remember that the function learn
is not stored in Person
, but in Student
. After we created a new instance lilly
of Student
,
var lilly = Object.create(Student);
lilly.name = 'Lilly';
its prototype chain will look like this:
lilly ------> Student ----> Person
name learn sayName
We can now access the fields name
, sayName
and learn
.
lilly.name; // -> 'Lilly'
lilly.sayName(); // -> 'I am Lilly'
lilly.learn('JavaScript'); // -> 'Learning all about JavaScript'
Practical Usage
Let's move on to some patterns that make everyday usage easier, but embrace the flexibilty of JavaScript's inheritance mechanism.
Constructors
So far, we have "customized" a new instance by just assigning to it. In some cases, however, this may be either a lot of work or not sophisticated enough.
To achieve what a constructor usally does, we can define the init
function as a standard.
Person.init = function (name, surname) {
this.name = name;
this.surname = surname;
// Do a sophisticated initialization here.
};
var john = Object.create(Person);
john.init('John', 'Doe');
In the init
function of a subclass, we may call init
from the "superclass":
var Student = Object.create(Person);
Student.init = function (name, surname, skill) {
Person.init.call(this, name, surname);
this.skill = skill;
};
var lilly = Object.create(Student);
lilly.init('Lilly', 'Doe', 'JavaScript');
We can still access all fields.
lilly.name; // -> 'Lilly'
lilly.surname; // -> 'Doe'
lilly.skill; // -> 'JavaScript'
lilly.sayName(); // -> 'My name is Lilly Doe'
lilly.learn(); // -> 'Learning all about JavaScript'
Object.assign
shorthand
You may have noticed, that creating a subclass with the pattern above is a bit cumbersome, especially if it has many fields, since we need to write Student.… = …
every single time.
To get closer to the good old class
notation, we can use Object.assign
.
var Student = Object.assign(Object.create(Person), {
init: function (name, surname, skill) {
Person.init.call(this, name, surname);
this.skill = skill;
},
learn: function () {
console.log('Learning all about ' + this.skill);
}
// …
});
This will create an object Student
which contains the fields init
and learn
, and has a prototype that points to Person
.
Student ----> Person
init sayName
learn
Constructor creator
Instead of doing a = Object.create(…); a.init(…);
, we can define another shorthand once and reuse it for every class.
var createConstructor = function (class) {
return function constructor () {
var instance = Object.create(class);
if (instance.init) instance.init.apply(instance, arguments);
return instance;
}
};
We can also define a shorthand create
…
var Person = {
create: constructor(Person),
init: function (…) {…},
…
};
and then comfortably create an instance of Person
:
var john = Person.create('John', 'Doe');
how does
relates to
which implies that Person is defined as
in terms of classes and prototypes? I'm pretty sure there is a difference except object and function, isn't it?
One difference I just found is that the function approach encapsulates the data. I found no way to access the data directly while it's been easy to do so when use a object as base.