Inheritance Flashcards
Explain prototypal inheritance
In javascript, every object has a hidden property [[prototype]] which can be accessed by the __proto__ accessor property. The __proto__ property cannot be assigned a value that would cause a cycle, and can only be null or Object.
When invoking a object property, if the property is not found at the obj, then we go up the prototype chain until we find the property.
Consider the following code. What is the expected output and why?
let animal = { eats: true, walk() { alert("Animal"); } }; let rabbit = { \_\_proto\_\_: animal }; rabbit.walk = function() { alert("Rabbit! Bounce-bounce!"); }; rabbit.walk();
Writing doesn’t use prototypes - the written property is assigned directly to the object. The only exception to this rule is accessor props.
What is the expected output of the following code?
let user = { name: "John", surname: "Smith", set fullName(value) { [this.name, this.surname] = value.split(" "); }, get fullName() { return `${this.name} ${this.surname}`; } }; let admin = { \_\_proto\_\_: user, isAdmin: true }; alert(admin.fullName); // John Smith (*) admin.fullName = "Alice Cooper"; // (**) alert(admin.fullName); alert(user.fullName);
Alice,Cooper
John,smith.
Accessor props are exceptions to the ‘writing doesn’t use prototype rules’ as assignment is handled by a setter function. So writing to such a property is actually the same as calling a function. Here in (**) we expect a property fullName to be defined in Admin, but what is actually being called is the fullName setter property in User, and then sets name and surname to Alice and Cooper in Admin(calling object).
Importantly, this behavior of this(this value of methods depending on the calling object) is critical so children classes only modify their own states and not the base object.
What is the output of the following code?
let animal = { eats: true }; let rabbit = { jumps: true, \_\_proto\_\_: animal }; alert(Object.keys(rabbit)); for(let prop in rabbit) alert(prop);
Object.keys() only returns jumps
For loop includes also the inherited properties so jumps, eats
. Note that this is an exception, most iterative methods do not consider inheritance.
If in a for-loop we want to only consider properties that are defined in the object itself we can filter those properties using the obj.hasOwnProperty() method.
What is prototype (regular property) and how does it differ from __proto__
They are two different concepts.
__proto__ is used to access [[prototype]] of an object, whereas prototype is applied to constructor functions to enforce that when the constructor is called with new, assign the object’s [[prototype]] to whatever the value of it’s prototype is.
There’s a default value for function.prototype:
Rabbit.prototype = { constructor: Rabbit };
Thus something like the following works:
let rabbit = new Rabbit(“White Rabbit”);
let rabbit2 = new rabbit.constructor(“Black Rabbit”);// Since its [[prototype]] is an object with a constructor property = Rabbit
Importantly Javascript doesn’t enforce or check what value we assign to prototype. If we set Rabbit.prototype = {} then the second line in the example above will likely throw an error.
Consequently, this means that to make a property accessible to all derived classes, the property must be defined in the prototype property of the constructor.I.e. all not private properties in classes are in fact assigned to the constructor’s prototype.
What is the output of the following code?
const obj = {};
alert(obj)
[object Object]
The built-in toString method defined Object is called. This because {} is syntactic sugar for new Object(), similar for arrays.
Thus:
alert(obj.__proto__ === Object.prototype); // true, recall that when a constructor is called, [[prototype]] is assigned to the value of prototype of the constructor.
How are methods made available to primitives?
When accessing properties of primitives, temporary wrapper objects are created using the corresponding built-in constructors(String, Number, Boolean) which provide access to methods temporarily.
The exceptions to this are null and undefined which do not have object wrappers.
I want to redefine the behavior of .split() methods on all strings. How do I do that?
Change native prototype
We can override the .split() method defined on the built-in String class:
String.prototype.split = …
Changing native prototypes is typically only recommended only for polyfilling - Making a substitute for a method that exists only in the Javascript specification but not yet supported by a particular Javascript engine.
What are classes?
Some say classes are just syntactic sugar since their type is a function, and what they do is essentially create a constructor function from the constructor defined in the class, and store the class methods in that function’s prototype.
However there are some important differences:
1. objects created with class has a special internal property [[IsClassConstructor]], which is used in several places, such as ensuring that the function must be called with new.
2. Class methods are not enumerable
3. Classes always use strict
How can I wrap an existing method from the base class with my own logic?
You can create a new function with the same name, thus overriding it, but within that function you can have access to the original method using ‘super’. Importantly, for class methods, arrow functions do not have access to super.
Does a class need a constructor?
We can define constructor() to override the default constructor:
constructor(…args) { super(…args) }
Why must we call super() in constructors of derived classes?
There’s a distinction between a constructor function of an inheriting class and other functions. A derived constructor has a special internal property [[ConstructorKind]]:”derived”.
That affects its behavior with new:
- When a regular function is executed with new, it creates an empty object and assigns it to this.
- When a derived constructor runs, it doesn’t do this. It expects the parent constructor to do this job.
Thus for derived constructors, super must be called which executes the parent constructor and initializes ‘this’. Importantly, super() must be called before any references to ‘this’
What is the expected output?
class Animal { name = 'animal'; constructor() { alert(this.name); // (*) } } class Rabbit extends Animal { name = 'rabbit'; } new Animal(); new Rabbit();
animal
animal
This is due to the class field initialization order:
- For base classes(no parents) the class field is initialized before the constructor
- For derived classes, the class field is initialized after super().
This is an exception to the behavior where we look at the nearer scope and then go outwards.
Why doesn’t the following mock implementation of super work? (replaced with this.__proto__….call(this))
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
In longEar, the call to eat() is reduced to:
rabbit.eat.call(longEar)
Then in rabbit, the call to eat() is reduced to:
longEar.__proto__.eat.call(longEar)
rabbit.eat.call(longEar)
which results in an infinite loop.
The solution is to have a hidden property for functions, [[HomeObject]]. When a function is defined on a class or object, its [[HomeObject]] property is assigned to that object, which super() uses to resolve the parent prototype and its methods.
let animal = { name: "Animal", eat() { // animal.eat.[[HomeObject]] == animal alert(`${this.name} eats.`); } }; let rabbit = { \_\_proto\_\_: animal, name: "Rabbit", eat() { // rabbit.eat.[[HomeObject]] == rabbit super.eat(); } }; let longEar = { \_\_proto\_\_: rabbit, name: "Long Ear", eat() { // longEar.eat.[[HomeObject]] == longEar super.eat(); } }; longEar.eat(); // Long Ear eats.
So the solution is to not use ‘this’. Instead, a method, such as longEar.eat, knows its [[HomeObject]] and takes the parent method from its prototype
What is [[HomeObject]] used for?
Functions in javascript are generally ‘free’(not bound) and can be copied between objection and called with another ‘this’
[[HomeObject]] violates that principle, since methods remember their objects, and [[HomeObject]] cannot changed.
[[HomeObject]] is only used in super(). If a method does not use super() then it can still be considered ‘free’ and can be copied freely between object.