Class inheritance, super
https://javascript.info/class-inheritance
Classes can extend one another. There’s a nice syntax, technically based on the prototypal inheritance.
To inherit from another class, we should specify "extends"
and the parent class before the brackets {..}
.
Here Rabbit
inherits from Animal
:
class Animal { constructor(name) { this.speed = 0; this.name = name; } run(speed) { this.speed += speed; alert(`${this.name} runs with speed ${this.speed}.`); } stop() { this.speed = 0; alert(`${this.name} stopped.`); } } // Inherit from Animal class Rabbit extends Animal { hide() { alert(`${this.name} hides!`); } } let rabbit = new Rabbit("White Rabbit"); rabbit.run(5); // White Rabbit runs with speed 5. rabbit.hide(); // White Rabbit hides!
The extends
keyword actually adds a [[Prototype]]
reference from Rabbit.prototype
to Animal.prototype
, just as you expect it to be, and as we’ve seen before.
So now rabbit
has access both to its own methods and to methods of Animal
.
extends
Class syntax allows to specify not just a class, but any expression after extends
.
For instance, a function call that generates the parent class:
function f(phrase) { return class { sayHi() { alert(phrase) } } } class User extends f("Hello") {} new User().sayHi(); // Hello
Here class User
inherits from the result of f("Hello")
.
That may be useful for advanced programming patterns when we use functions to generate classes depending on many conditions and can inherit from them.
Overriding a method
Now let’s move forward and override a method. As of now, Rabbit
inherits the stop
method that sets this.speed = 0
from Animal
.
If we specify our own stop
in Rabbit
, then it will be used instead:
class Rabbit extends Animal {
stop() {
// ...this will be used for rabbit.stop()
}
}
…But usually we don’t want to totally replace a parent method, but rather to build on top of it, tweak or extend its functionality. We do something in our method, but call the parent method before/after it or in the process.
Classes provide "super"
keyword for that.
super.method(...)
to call a parent method.super(...)
to call a parent constructor (inside our constructor only).
For instance, let our rabbit autohide when stopped:
class Animal { constructor(name) { this.speed = 0; this.name = name; } run(speed) { this.speed += speed; alert(`${this.name} runs with speed ${this.speed}.`); } stop() { this.speed = 0; alert(`${this.name} stopped.`); } } class Rabbit extends Animal { hide() { alert(`${this.name} hides!`); } stop() { super.stop(); // call parent stop this.hide(); // and then hide } } let rabbit = new Rabbit("White Rabbit"); rabbit.run(5); // White Rabbit runs with speed 5. rabbit.stop(); // White Rabbit stopped. White rabbit hides!
Now Rabbit
has the stop
method that calls the parent super.stop()
in the process.
super
As was mentioned in the chapter Arrow functions revisited, arrow functions do not have super
.
If accessed, it’s taken from the outer function. For instance:
class Rabbit extends Animal {
stop() {
setTimeout(() => super.stop(), 1000); // call parent stop after 1sec
}
}
The super
in the arrow function is the same as in stop()
, so it works as intended. If we specified a “regular” function here, there would be an error:
// Unexpected super
setTimeout(function() { super.stop() }, 1000);
Overriding constructor
With constructors it gets a little bit tricky.
Till now, Rabbit
did not have its own constructor
.
According to the specification, if a class extends another class and has no constructor
, then the following constructor
is generated:
class Rabbit extends Animal { // generated for extending classes without own constructors constructor(...args) { super(...args); } }
As we can see, it basically calls the parent constructor
passing it all the arguments. That happens if we don’t write a constructor of our own.
Now let’s add a custom constructor to Rabbit
. It will specify the earLength
in addition to name
:
class Animal { constructor(name) { this.speed = 0; this.name = name; } // ... } class Rabbit extends Animal { constructor(name, earLength) { this.speed = 0; this.name = name; this.earLength = earLength; } // ... } // Doesn't work! let rabbit = new Rabbit("White Rabbit", 10); // Error: this is not defined.
Whoops! We’ve got an error. Now we can’t create rabbits. What went wrong?
The short answer is: constructors in inheriting classes must call super(...)
, and (!) do it before using this
.
…But why? What’s going on here? Indeed, the requirement seems strange.
Of course, there’s an explanation. Let’s get into details, so you’d really understand what’s going on.
In JavaScript, there’s a distinction between a “constructor function of an inheriting class” and all others. In an inheriting class, the corresponding constructor function is labelled with a special internal property [[ConstructorKind]]:"derived"
.
The difference is:
- When a normal constructor runs, it creates an empty object as
this
and continues with it. - But when a derived constructor runs, it doesn’t do it. It expects the parent constructor to do this job.
So if we’re making a constructor of our own, then we must call super
, because otherwise the object with this
reference to it won’t be created. And we’ll get an error.
For Rabbit
to work, we need to call super()
before using this
, like here:
class Animal { constructor(name) { this.speed = 0; this.name = name; } // ... } class Rabbit extends Animal { constructor(name, earLength) { super(name); this.earLength = earLength; } // ... } // now fine let rabbit = new Rabbit("White Rabbit", 10); alert(rabbit.name); // White Rabbit alert(rabbit.earLength); // 10
Super: internals, [[HomeObject]]
Let’s get a little deeper under the hood of super
. We’ll see some interesting things by the way.
First to say, from all that we’ve learned till now, it’s impossible for super
to work.
Yeah, indeed, let’s ask ourselves, how it could technically work? When an object method runs, it gets the current object as this
. If we call super.method()
then, how to retrieve the method
? Naturally, we need to take the method
from the prototype of the current object. How, technically, we (or a JavaScript engine) can do it?
Maybe we can get the method from [[Prototype]]
of this
, as this.__proto__.method
? Unfortunately, that doesn’t work.
Let’s try to do it. Without classes, using plain objects for the sake of simplicity.
Here, rabbit.eat()
should call animal.eat()
method of the parent object:
let animal = { name: "Animal", eat() { alert(`${this.name} eats.`); } }; let rabbit = { __proto__: animal, name: "Rabbit", eat() { // that's how super.eat() could presumably work this.__proto__.eat.call(this); // (*) } }; rabbit.eat(); // Rabbit eats.
At the line (*)
we take eat
from the prototype (animal
) and call it in the context of the current object. Please note that .call(this)
is important here, because a simple this.__proto__.eat()
would execute parent eat
in the context of the prototype, not the current object.
And in the code above it actually works as intended: we have the correct alert
.
Now let’s add one more object to the chain. We’ll see how things break:
let animal = { name: "Animal", eat() { alert(`${this.name} eats.`); } }; let rabbit = { __proto__: animal, eat() { // ...bounce around rabbit-style and call parent (animal) method this.__proto__.eat.call(this); // (*) } }; let longEar = { __proto__: rabbit, eat() { // ...do something with long ears and call parent (rabbit) method this.__proto__.eat.call(this); // (**) } }; longEar.eat(); // Error: Maximum call stack size exceeded
The code doesn’t work anymore! We can see the error trying to call longEar.eat()
.
It may be not that obvious, but if we trace longEar.eat()
call, then we can see why. In both lines (*)
and (**)
the value of this
is the current object (longEar
). That’s essential: all object methods get the current object as this
, not a prototype or something.
So, in both lines (*)
and (**)
the value of this.__proto__
is exactly the same: rabbit
. They both call rabbit.eat
without going up the chain in the endless loop.
Here’s the picture of what happens:
Inside
longEar.eat()
, the line(**)
callsrabbit.eat
providing it withthis=longEar
.// inside longEar.eat() we have this = longEar this.__proto__.eat.call(this) // (**) // becomes longEar.__proto__.eat.call(this) // that is rabbit.eat.call(this);
Then in the line
(*)
ofrabbit.eat
, we’d like to pass the call even higher in the chain, butthis=longEar
, sothis.__proto__.eat
is againrabbit.eat
!// inside rabbit.eat() we also have this = longEar this.__proto__.eat.call(this) // (*) // becomes longEar.__proto__.eat.call(this) // or (again) rabbit.eat.call(this);
…So
rabbit.eat
calls itself in the endless loop, because it can’t ascend any further.
The problem can’t be solved by using this
alone.
[[HomeObject]]
To provide the solution, JavaScript adds one more special internal property for functions: [[HomeObject]]
.
When a function is specified as a class or object method, its [[HomeObject]]
property becomes that object.
This actually violates the idea of “unbound” functions, because methods remember their objects. And [[HomeObject]]
can’t be changed, so this bound is forever. So that’s a very important change in the language.
But this change is safe. [[HomeObject]]
is used only for calling parent methods in super
, to resolve the prototype. So it doesn’t break compatibility.
Let’s see how it works for super
– again, using plain objects:
let animal = { name: "Animal", eat() { // [[HomeObject]] == animal alert(`${this.name} eats.`); } }; let rabbit = { __proto__: animal, name: "Rabbit", eat() { // [[HomeObject]] == rabbit super.eat(); } }; let longEar = { __proto__: rabbit, name: "Long Ear", eat() { // [[HomeObject]] == longEar super.eat(); } }; longEar.eat(); // Long Ear eats.
Every method remembers its object in the internal [[HomeObject]]
property. Then super
uses it to resolve the parent prototype.
[[HomeObject]]
is defined for methods defined both in classes and in plain objects. But for objects, methods must be specified exactly the given way: as method()
, not as "method: function()"
.
In the example below a non-method syntax is used for comparison. [[HomeObject]]
property is not set and the inheritance doesn’t work:
let animal = { eat: function() { // should be the short syntax: eat() {...} // ... } }; let rabbit = { __proto__: animal, eat: function() { super.eat(); } }; rabbit.eat(); // Error calling super (because there's no [[HomeObject]])
Static methods and inheritance
The class
syntax supports inheritance for static properties too.
For instance:
class Animal {
constructor(name, speed) {
this.speed = speed;
this.name = name;
}
run(speed = 0) {
this.speed += speed;
alert(`${this.name} runs with speed ${this.speed}.`);
}
static compare(animalA, animalB) {
return animalA.speed - animalB.speed;
}
}
// Inherit from Animal
class Rabbit extends Animal {
hide() {
alert(`${this.name} hides!`);
}
}
let rabbits = [
new Rabbit("White Rabbit", 10),
new Rabbit("Black Rabbit", 5)
];
rabbits.sort(Rabbit.compare);
rabbits[0].run(); // Black Rabbit runs with speed 5.
Now we can call Rabbit.compare
assuming that the inherited Animal.compare
will be called.
How does it work? Again, using prototypes. As you might have already guessed, extends also gives Rabbit
the [[Prototype]]
reference to Animal
.
So, Rabbit
function now inherits from Animal
function. And Animal
function normally has [[Prototype]]
referencing Function.prototype
, because it doesn’t extend
anything.
Here, let’s check that:
class Animal {}
class Rabbit extends Animal {}
// for static propertites and methods
alert(Rabbit.__proto__ === Animal); // true
// and the next step is Function.prototype
alert(Animal.__proto__ === Function.prototype); // true
// that's in addition to the "normal" prototype chain for object methods
alert(Rabbit.prototype.__proto__ === Animal.prototype);
This way Rabbit
has access to all static methods of Animal
.
No static inheritance in built-ins
Please note that built-in classes don’t have such static [[Prototype]]
reference. For instance, Object
has Object.defineProperty
, Object.keys
and so on, but Array
, Date
etc do not inherit them.
Here’s the picture structure for Date
and Object
:
Note, there’s no link between Date
and Object
. Both Object
and Date
exist independently. Date.prototype
inherits from Object.prototype
, but that’s all.
Such difference exists for historical reasons: there was no thought about class syntax and inheriting static methods at the dawn of JavaScript language.
Natives are extendable
Built-in classes like Array, Map and others are extendable also.
For instance, here PowerArray
inherits from the native Array
:
// add one more method to it (can do more)
class PowerArray extends Array {
isEmpty() {
return this.length === 0;
}
}
let arr = new PowerArray(1, 2, 5, 10, 50);
alert(arr.isEmpty()); // false
let filteredArr = arr.filter(item => item >= 10);
alert(filteredArr); // 10, 50
alert(filteredArr.isEmpty()); // false
Please note one very interesting thing. Built-in methods like filter
, map
and others – return new objects of exactly the inherited type. They rely on the constructor
property to do so.
In the example above,
arr.constructor === PowerArray
So when arr.filter()
is called, it internally creates the new array of results exactly as new PowerArray
. And we can keep using its methods further down the chain.
Even more, we can customize that behavior. The static getter Symbol.species
, if exists, returns the constructor to use in such cases.
For example, here due to Symbol.species
built-in methods like map
, filter
will return “normal” arrays:
class PowerArray extends Array { isEmpty() { return this.length === 0; } // built-in methods will use this as the constructor static get [Symbol.species]() { return Array; } } let arr = new PowerArray(1, 2, 5, 10, 50); alert(arr.isEmpty()); // false // filter creates new array using arr.constructor[Symbol.species] as constructor let filteredArr = arr.filter(item => item >= 10); // filteredArr is not PowerArray, but Array alert(filteredArr.isEmpty()); // Error: filteredArr.isEmpty is not a function
We can use it in more advanced keys to strip extended functionality from resulting values if not needed. Or, maybe, to extend it even further.
Tasks
Here’s the code with Rabbit
extending Animal
.
Unfortunately, Rabbit
objects can’t be created. What’s wrong? Fix it.
class Animal { constructor(name) { this.name = name; } } class Rabbit extends Animal { constructor(name) { this.name = name; this.created = Date.now(); } } let rabbit = new Rabbit("White Rabbit"); // Error: this is not defined alert(rabbit.name);
We’ve got a Clock
class. As of now, it prints the time every second.
Create a new class ExtendedClock
that inherits from Clock
and adds the parameter precision
– the number of ms
between “ticks”. Should be 1000
(1 second) by default.
- Your code should be in the file
extended-clock.js
- Don’t modify the original
clock.js
. Extend it.
As we know, all objects normally inherit from Object.prototype
and get access to “generic” object methods like hasOwnProperty
etc.
For instance:
class Rabbit { constructor(name) { this.name = name; } } let rabbit = new Rabbit("Rab"); // hasOwnProperty method is from Object.prototype // rabbit.__proto__ === Object.prototype alert( rabbit.hasOwnProperty('name') ); // true
But if we spell it out explicitly like "class Rabbit extends Object"
, then the result would be different from a simple "class Rabbit"
?
What’s the difference?
Here’s an example of such code (it doesn’t work – why? fix it?):
class Rabbit extends Object {
constructor(name) {
this.name = name;
}
}
let rabbit = new Rabbit("Rab");
alert( rabbit.hasOwnProperty('name') ); // true
'C Lang > JS Technic' 카테고리의 다른 글
class선언, class사용법, extends, 부모의 생성자를 받는 super, 부모의 함수를 받는 super 예쩨 (0) | 2018.05.20 |
---|---|
promise 잘 모르겠는 부분들 이해하기(then에 대하여) (0) | 2018.05.16 |
generator 객체2(JavaScript の ジェネレータ を極める!) (0) | 2018.05.14 |
generator 객체1 (function *, next(), yield, promise와 yield,co모듈과 yield) (0) | 2018.05.14 |
Node.jsのexportsについて (0) | 2018.05.13 |