Skip to content

Instantly share code, notes, and snippets.

@derhuerst
Last active June 29, 2023 17:54
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save derhuerst/a585c4916b1c361cc6f0 to your computer and use it in GitHub Desktop.
Save derhuerst/a585c4916b1c361cc6f0 to your computer and use it in GitHub Desktop.
Prototypal Inheritance in JavaScript

Prototypal Inheritance

This way of doing inheritance is taken from JS Objects: Deconstruction.

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 janes 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. janes 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';

johns 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');
@OvisMaximus
Copy link

how does

var john = Person.create('John', 'Doe');

relates to

var john = new Person('John', 'Doe');

which implies that Person is defined as

function Person(name, surname) {
        this.name = name;
        this.surname = surname;
}

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.

@serapath
Copy link

serapath commented Aug 20, 2016

@derhuerst
Copy link
Author

@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 to this.

@serapath
Copy link

serapath commented Aug 22, 2016

fixed it above. how do you like it now?

@derhuerst
Copy link
Author

  1. Object.create is magic too - maybe more than new which people might know from other contexts

But new is misleading since people think of classes when they use it, which JavaScript doesn't have.

  1. this is something that cannot be avoided, because it has to be used in "prototype methods" anyway

You're right. Still, i try to minimize it when consuming libraries or when letting other consume libraries. Inside of library code it's fine imho.

I actually like the __proto__ solution, because many browsers implemented it already when it was not a standard, so it's supported and is now with es6 it's even a standard. It allows to set directly the one thing that we care about in all this, which is: the prototype chain of objects :-)

There's a fair point in this.

Regarding your ESx versions:

  • ES6: I'd use Object.create here. I don't like the idea that something to fundamental like scope chains is visible in the shape of __proto__.
  • ES5 Classic Version: I think this version is even more magic (using instanceof and new) and also invites people to think of classes. I like ES5 more.

@serapath
Copy link

Now that __proto__ is an official ES6 standard anyway, why not?
When you try to explain somebody what is going on, you tell them about the prototype chain and how it's set.
The __proto__ makes exactly that, what you explain in those situations, explicitly visible :-)
Because it's now a standard you can rely on it and because old browsers wont change anymore and implemented it voluntarily it even works in old browsers 🌻

With the use of __proto__ you can completely get rid of new and Object.create and in the future even of Object.assign - which is not even necessary, because it's just implementing "extend" as a method which can be manually coded up in 1-2 lines of code anyway...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment