This way of doing inheritance is taken from JS Objects: Deconstruct
ion.
Also check the slides of The four layers of JavaScript OOP.
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'
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'
Let's move on to some patterns that make everyday usage easier, but embrace the flexibilty of JavaScript's inheritance mechanism.
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'
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
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');
@serapath The first ot the two solutions is what I like more. The second is just too magic, with
self.__proto__ = …
.Regarding the first one:
I don't really a benefit over my solution. I feel like building the instance manually (
x = Object.create(X); x.foo = 'bar'
) is still easier to grasp than assigning tothis
.