Inheritance Flashcards

1
Q

Explain prototypal inheritance

A

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.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
2
Q

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();
A

Writing doesn’t use prototypes - the written property is assigned directly to the object. The only exception to this rule is accessor props.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
3
Q

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);
A

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.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
4
Q

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);
A

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.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
5
Q

What is prototype (regular property) and how does it differ from __proto__

A

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.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
6
Q

What is the output of the following code?
const obj = {};
alert(obj)

A

[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 well did you know this?
1
Not at all
2
3
4
5
Perfectly
7
Q

How are methods made available to primitives?

A

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.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
8
Q

I want to redefine the behavior of .split() methods on all strings. How do I do that?

A

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.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
9
Q

What are classes?

A

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 well did you know this?
1
Not at all
2
3
4
5
Perfectly
10
Q

How can I wrap an existing method from the base class with my own logic?

A

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.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
11
Q

Does a class need a constructor?

A

We can define constructor() to override the default constructor:
constructor(…args) { super(…args) }

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
12
Q

Why must we call super() in constructors of derived classes?

A

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’

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
13
Q

What is the expected output?

class Animal {
  name = 'animal';

  constructor() {
    alert(this.name); // (*)
  }
}

class Rabbit extends Animal {
  name = 'rabbit';
}

new Animal();
new Rabbit();
A

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.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
14
Q

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
A

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

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
15
Q

What is [[HomeObject]] used for?

A

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.

How well did you know this?
1
Not at all
2
3
4
5
Perfectly
16
Q

What is the output of the following code?

let animal = {
  sayHi() {
    alert(`I'm an animal`);
  }
};

let rabbit = {
  \_\_proto\_\_: animal,
  sayHi() {
    super.sayHi();
  }
};

let plant = {
  sayHi() {
    alert("I'm a plant");
  }
};

// tree inherits from plant
let tree = {
  \_\_proto\_\_: plant,
  sayHi: rabbit.sayHi // (*)
};

tree.sayHi();
A

I’m an animal

The reason is that the sayHi method is assigned to rabbit.sayHi, whose [[HomeObject]] is assigned to rabbit, so when it’s called is calls super.sayHi whose [[HomeObject]] is assigned to animal. So even though the prototype of tree is plant, the actual invocation of sayHi comes from animal.

Importantly, for [[HomeObject]] to be assigned correctly, the methods for object literals MUST be defined as { method(){} } and not { method: function (){} }
E.g.

let animal = {
  eat: function() { // intentionally writing like this instead of eat() {...
    // ...
  }
};

let rabbit = {
  \_\_proto\_\_: animal,
  eat: function() {
    super.eat();
  }
};

rabbit.eat();  // Error calling super as there's no [[HomeObject]]
17
Q

In what ways can we define static methods and properties?

A

class User {
static method() {}
}

OR

User.staticMethod = function() {}

18
Q

How is accessibility of methods and properties controlled in classes?

A

In OOP there are two groups where methods and props belong to:
- Internal interface: Accessible from other methods of the class but not outside
- External interface: Accessible from outside the class

19
Q

What is the output of the following?

class PowerArray extends Array {
  isEmpty() {
    return this.length === 0;
  }
}

let arr = new PowerArray(1, 2, 5, 10, 50);
const newArr = arr.filter(el => el >= 5);
alert(newArr.constructor === Array)
A

False. The cool thing about built-in methods like filter and map is that the return new objects of exactly the inherited type. Internally it creates a new array using arr.constructor. This means that the returned array still has access to PowerArray’s methods etc.

Furthermore, we can define a special static that returns the constructor to be used by javascript when creating new entities in the built-in methods:

class PowerArray extends Array {
    static get [Symbol.species]() { return Array }
}

let arr = new PowerArray();
const newArr = arr.filter(el => el>=5);
alert(newArr.constructor === Array) //true
20
Q

Are static methods in built-ins inherited by other built-ins?

A

No. We know that all classes extend Object, and so normally both static and non-static methods should be inherited. The exception is built-in classes.

E.g Date extends Object, so their instance have methods from Object.prototype, but Date.__proto__ does not reference Object, so there’s no such thing as Date.keys()

Recall that usually for a object1 to extend object2, object1.prototype = object2.prototype. But for built-ins, Date.prototype !== Object.prototype.

21
Q

Describe how the instanceof operator works?

A

instanceof is used to check whether an object belongs to a certain class, taking into account inheritance.

How it works is:
1. If there is a static getter [Symbol.hasInstance] method, then return the result of calling it
2. Else, go up obj’s prototype chain(using __proto__) and check if any of them are equal to Class.prototype.

Importantly, the constructor of the class is not considered when using instanceof. That is:

const rabbit = new Rabbit();
Rabbit.prototype = {};

alert(rabbit instance of Rabbit) //false

22
Q

What are mixins and how to define and use them?

A

Javascript does not support multiple inheritance. In the event we do need an object to be a mix of two or more classes, then we can use mixins. Mixins are defined as classes containing methods that can be used by other classes without a need to inherit from it.

Define a Mixin:

const eventMixin = {
  /**
   * Subscribe to event, usage:
   *  menu.on('select', function(item) { ... }
  */
  on(eventName, handler) {
    if (!this._eventHandlers) this._eventHandlers = {};
    if (!this._eventHandlers[eventName]) {
      this._eventHandlers[eventName] = [];
    }
    this._eventHandlers[eventName].push(handler);
  },

  /**
   * Cancel the subscription, usage:
   *  menu.off('select', handler)
   */
  off(eventName, handler) {
    let handlers = this._eventHandlers?.[eventName];
    if (!handlers) return;
    for (let i = 0; i < handlers.length; i++) {
      if (handlers[i] === handler) {
        handlers.splice(i--, 1);
      }
    }
  },

  /**
   * Generate an event with the given name and data
   *  this.trigger('select', data1, data2);
   */
  trigger(eventName, ...args) {
    if (!this._eventHandlers?.[eventName]) {
      return; // no handlers for that event name
    }

    // call the handlers
    this._eventHandlers[eventName].forEach(handler => handler.apply(this, args));
  }
};

Use the mixin:

// Make a class
class Menu {
  choose(value) {
    this.trigger("select", value);
  }
}
// Add the mixin with event-related methods
Object.assign(Menu.prototype, eventMixin);

let menu = new Menu();

// add a handler, to be called on selection:
menu.on("select", value => alert(`Value selected: ${value}`));

// triggers the event => the handler above runs and shows:
// Value selected: 123
menu.choose("123");
23
Q
A