As has been briefly mentioned previously, inheritance in JavaScript is archived by using prototypes of constructor functions. In JavaScript, a prototype is an object that provides shared properties for other objects. And only a function object has a prototype because only a function object is callable and can create other objects. In ES6, arrow functions don't have prototypes. We will discuss that later.
You can think of a function as a factory and its prototype is the specification of the products that the factory manufactures. Every time you call a function with the new keyword, you place an order for its product. And the factory will produce it according to how it is specified in the prototype.
Now, let's see how inheritance works in code. We will create another constructor function, called TeamMember, and it will inherit properties from User and also override the greeting() method and provide a new method called work(). Later, we will add eat() method to User and move() to Object.
Here is how it is implemented in ES5:
1. function User (name, interests) {
2. this.name = name;
3. this.interests = interests;
4. }
5. User.prototype.greeting = function () {
6. console.log('Hi, I\'m ' + this.name + '.');
7. }
In lines 1 to 4, we create a User constructor function. And what it really does is create a function object using the Function constructor. In JavaScript, you can check who created an object by using its constructor property, which references back to its creator, as follows:
console.log(User.constructor === Function); // true
And once the User constructor function is created, it has a prototype object. And a User prototype object itself is created by the User constructor function, as you can see in the following:
console.log(User.prototype.constructor === User); // true
And in JavaScript, after you create a user object using the User constructor function, that object will have a __proto__ property that references the User prototype object. You can see the link like this:
var user = new User();
console.log(user.__proto__ === User.prototype); // true
This __proto__ reference serves as a link in the prototype chain. You will see what that means visually later.
Now back to the code. In lines 5 to 7, we create a greeting property on the User prototype. This will create a method that can be inherited by User subclasses. And, as we mentioned earlier, if you define the greeting method inside the User constructor function, subclasses won't see this greeting method. We will see the reason for this shortly:
8. function TeamMember (name, interests, tasks) {
9. User.call(this, name, interests);
10. this.tasks = tasks;
11. }
12. TeamMember.prototype = Object.create(User.prototype);
13. TeamMember.prototype.greeting = function () {
14. console.log('I\'m ' + this.name + '. Welcome to the team!');
15. };
16. TeamMember.prototype.work = function () {
17. console.log('I\'m working on ' + this.tasks.length + ' tasks');
18. };
In lines 8 to 13, we create a TeamMember constructor function, and inside it, we invoke the User constructor function's call() method, which is inherited from the Function object to chain constructors, which is similar to invoking super() in a constructor of a Java class. One difference is that the call() method's first argument must be an object, which serves as the execution context. In our example, we use this as the execution context. Inside the call() method, the name and interests properties are initialized. And then, we add an additional property, tasks, to TeamMember.
In line 12, we use Object.create() to create a TeamMember prototype object using the User prototype object. In this way, objects created by the TeamMember constructor function will have the properties of the User prototype object and each team member object will have a __proto__ property that links to this TeamMember prototype.
In lines 13 to 15, we override the original greeting() method of the User prototype so that objects created by the TeamMember constructor function will have different behavior. This will not affect the User prototype object since they are essentially two different objects, even though these two prototype objects have the same constructor, as you can see in the following:
console.log(User.prototype === TeamMember.prototype); // false
console.log(User.prototype.constructor === TeamMember.prototype.constructor); // true
In lines 16 to 18, we add a new method, work(), to the TeamMember prototype object. In this way, objects created by the TeamMember constructor function will have this additional behavior:
19. var member = new TeamMember('Sunny', ['Traveling'],
20. ['Buy three tickets','Book a hotel']);
21. member.greeting(); // I'm Sunny. Welcome to the team!
22. member.work(); // I'm working on 2 tasks
23
24. console.log(member instanceof TeamMember); // true
25. console.log(member instanceof User); // true
26. console.log(member instanceof Object); // true
27
28. User.prototype.eat = function () {
29. console.log('What will I have for lunch?');
30. };
31. member.eat(); // What will I have for lunch?
32
33. // Add a method to the top
34. Object.prototype.move = function () {
35. console.log('Every object can move now');
36. };
37. member.move(); // Every object can move now
38. var alien = {};
39. alien.move(); // Every object can move now
40. User.move(); // Even the constructor function
In line 19, we create a member object using the TeamMember constructor function. Line 21 shows that the member object can greet in a different way to objects created by the User constructor function. And line 22 shows that the member object can work.
Lines 24 to 26 show that the member object is an instance of all its superclasses.
In lines 28 to 30, we add the eat() method to the User prototype, and even though the member object is created before this, as you can see from line 31, it also inherits that method.
In line 34, we add the move() method to the Object prototype, which might turn out to be a really bad idea since, as you can see in lines 37 to 40, every object can move now, even those constructor function objects.
We just create an inheritance chain starting from Object | User | TeamMember. The prototype link is the key to this chain. Here is how it appears:
Figure 1.2: Prototype-based inheritance
On the left-hand side are the constructor functions, and on the right-hand side are their corresponding prototypes. The bottom is the member object. As you can see, the member object's __proto__ property references the prototype object of TeamMember. And the __proto__ property of the TeamMember prototype object itself references the prototype object of User. And the __proto__ property of the User prototype object references the top level, which is the prototype object of Object. To verify the link, you can do something like this:
console.log(member.__proto__ === TeamMember.prototype); // true
console.log(TeamMember.prototype.__proto__ === User.prototype); // true
console.log(User.prototype.__proto__ === Object.prototype); // true
So, be really careful with the __proto__ property. If you, let's say, accidentally change this property to something else, the inheritance will break:
User.prototype.__proto__ = null;
member.move(); // Uncaught TypeError: member.move is not a function
console.log(member instanceof Object); // false (Oops!)
It is recommended to use Object.prototype.isPrototypeof() to check the prototype chain:
TeamMember.prototype.isPrototypeOf(member); // true
With the inheritance relationship map showing in the preceding diagram, you can easily see how JavaScript resolves a property through the prototype chain. For example, when you access a member object's name property, JavaScript finds that it is on the object itself and will not go up the chain. And when you access the move() method, JavaScript will go up the chain and check whether the TeamMember prototype has it and, since it doesn't, JavaScript will keep going up until it finds the method in the Object prototype. You can use an object's hasOwnProperty() method to check whether that object has a property as its own instead of inherited through the prototype chain:
member.hasOwnProperty('name'); // true
member.hasOwnProperty('move'); // false