Chapter 5 inheritance of JavaScript object-oriented Essentials

Chapter 5 succession

Essentials of object oriented JavaScript -- Nicholas C. Zakas
The syntax of class already exists in ES6

Learning how to create objects is the first step in understanding object-oriented programming. The second step is to understand inheritance.
In traditional object-oriented languages, classes inherit the properties of other classes.
However, in JavaScript, you can inherit between objects without a class structure that defines a relationship.
This inheritance mechanism is one you are already familiar with: prototype.

1. Prototype chain and object prototype

The built-in inheritance method of JavaScript is called prototype linking or prototype inheritance.
As you learned in Chapter 4, prototype properties can be used automatically on object instances, which is a form of inheritance.
Object instances inherit properties from the prototype.
Because the prototype is also an object, it has its own prototype and inherits the prototype.
This is the prototype chain: an object inherits from its prototype, which in turn inherits from its prototype, and so on.

Unless you specify otherwise, all objects (including your own defined objects) are automatically inherited from Object (which will be discussed later in this chapter).
More specifically, all objects inherit from object prototype.
Any object defined by an object literal sets its [[prototype]] to object Prototype, which means it's from object Prototype inherits properties, just like book in this example:

var book = {
    title: "The Principles of Object-Oriented JavaScript"
};

var prototype = Object.getPrototypeOf(book);

console.log(prototype === Object.prototype);        // true

Here, book has a value equal to object Prototype.
No additional code is required to achieve this because this is the default behavior when creating new objects.
This relationship means that the book is automatically removed from object Prototype receiving method.

1.1. Object. Methods on Prototype

Several methods used in the past few chapters are actually in object It is defined on the prototype and is therefore inherited by all other objects. Those methods are:

  • hasOwnProperty(): determines whether there is a self owned property with the given name
  • propertyIsEnumerable(): determines whether its own properties are enumerable
  • isPrototypeOf(): determines whether an object is the prototype of another object
  • valueOf(): returns the value representation of the object
  • toString(): returns the string representation of the object

These five methods appear on all objects through inheritance.
The last two are important when you need to make objects work consistently in JavaScript, and sometimes you may want to define them yourself.

1.1.1. valueOf()

The valueOf() method is called whenever an operator is used on an object.
By default, valueOf() returns only object instances.
The original wrapper type overrides valueOf() so that it returns a String of String, Boolean, and Number.
Similarly, the valueOf() method of the Date object returns the era time in milliseconds (just like Date.prototype.getTime()).
This allows you to write code that compares dates, for example:

var now = new Date();
var earlier = new Date(2010, 1, 1);

console.log(now > earlier); // true

In this example, now is the date representing the current time, and earlier is a fixed date in the past.
When the greater than operator (>) is used, the valueOf() method is called on both objects before the comparison is performed.
You can even subtract a date from another date because valueOf() returns the number of milliseconds from the beginning of the era.

If you want to use an object for an operator, you can always define your own valueOf() method.
If you do define the valueOf() method, remember that you are not changing how the operator works, but only using the value of the operator's default behavior.

1.1.2. toString()

Whenever JavaScript expects a string, it also implicitly calls the original value.
For example, when a string is used as an operand of the addition operator, the other operand is automatically converted to a string.
If the other operand is the original value, it is converted to a string representation (for example, true becomes "true"), but if it is a reference value, valueOf() is called.
If valueOf() returns a reference value, toString() is called and the returned value is used. For example:

var book = {
    title: "The Principles of Object-Oriented JavaScript"
};

var message = "Book = " + book;

console.log(message); // "Book = [object Object]"

This code constructs a string by combining "book =" with book.
Since book is an object, its toString() method is called.
This method inherits from object Prototype and returns the default value "[object]" in most JavaScript engines.
If you are satisfied with this value, you do not need to change the toString() method of the object.
However, sometimes it is useful to define your own toString() method so that the string conversion will return a value that provides more information.
For example, suppose you want the previous script to record the title of the book:

var book = {
    title: "The Principles of Object-Oriented JavaScript",
    toString: function() {
        return "[Book " + this.title + "]"
    }
};

var message = "Book = " + book;

// "Book = [Book The Principles of Object-Oriented JavaScript]"
console.log(message);

This code defines a custom toString() method that returns a more useful value than the inherited version.
You usually don't need to worry about defining custom toString() methods, but it's best to know that you can do so if necessary.

1.2. Modify object prototype

By default, all objects are from object Prototype inherits, so object Changes to the prototype affect all objects.
That's a very dangerous situation.
In Chapter 4, it is recommended that you do not modify the built-in object prototype, and for object Prototype should be more careful. See what happens:

Object.prototype.add = function(value) {
    return this + value;
};

var book = {
    title: "The Principles of Object-Oriented JavaScript"
};

console.log(book.add(5)); // "[object Object]5"
console.log("title".add("end")); // "titleend"

// in a web browser
console.log(document.add(true)); // "[object HTMLDocument]true"
console.log(window.add(5)); // "[object Window]true"

Add object prototype. add() causes all objects to have an add() method, whether it really makes sense or not.
This problem is not only for developers, but also for the JavaScript Language Committee: it must put new methods in different places,
Because to object Adding a prototype method can have unpredictable consequences.

Another aspect of this problem involves adding to object Add enumerable properties to prototype.
In the previous example, object prototype. Add () is an enumerable property, which means that it will be displayed when you use the for in loop, for example:

var empty = {};

for (var property in empty) {
    console.log(property);
}

Here, an empty object will still output "add" as an attribute because it exists on the prototype and is enumerable.
Considering the frequency of using for in in JavaScript, use enumerable attributes to modify object Prototype can affect a lot of code.
So, Douglas Crockford It is recommended to always use hasOwnProperty() in a for in loop, for example:

var empty = {};

for (var property in empty) {
    if (empty.hasOwnProperty(property)) {
        console.log(property);
    }
}

Although this method is effective for prototype attributes that may not be needed, it also limits the use of for in only for its own attributes, which may or may not be what you want.
The most flexible and best option is not to modify the object prototype.

2. Object inheritance

The simplest type of inheritance is between objects. All you have to do is specify which object should be the [[Prototype]] of the new object.
Object literal [[prototype]] is implicitly set to object Prototype, but you can also use object The create () method explicitly specifies [[prototype]].

Object. The create () method accepts two parameters.
The first parameter specifies the value of [[Prototype]] for the new object.
The optional second parameter specifies the attribute descriptor object, followed by object Defineproperties () uses the same format (see Chapter 3). Consider the following:

var book = {
    title: "The Principles of Object-Oriented JavaScript"
};

// is the same as
var book = Object.create(Object.prototype, {
    title: {
        configurable: true,
        enumerable: true,
        value: "The Principles of Object-Oriented JavaScript",
        writable: true
    }
});

The two declarations in this code are actually the same.
The first declaration uses object literals to define an object with a single attribute called title.
The object is automatically removed from object Prototype inherits, and this attribute is set to configurable, enumerable and writable by default.
The second declaration does the same thing, but uses object Create() is explicitly declared.
The behavior of the book object generated in each declaration is exactly the same.
But you may never write directly from object Prototype inherits the code, because it will be obtained by default. Inheriting other objects is more interesting:

var person1 = {
    name: "Nicholas",
    sayName: function() {
        console.log(this.name);
    }
};

var person2 = Object.create(person1, {
    name: {
        configurable: true,
        enumerable: true,
        value: "Greg",
        writable: true
    }
});

person1.sayName(); // outputs "Nicholas"
person2.sayName(); // outputs "Greg"

console.log(person1.hasOwnProperty("sayName")); // true
console.log(person1.isPrototypeOf(person2)); // true
console.log(person2.hasOwnProperty("sayName")); // false

This code creates an object person1 using the name attribute and the sayName() method.
The person2 object inherits from person1, so it inherits name and sayName().
However, person2 is through object Defined by create (), it also defines its own name attribute for person2.
This attribute masks the prototype attribute with the same name and can be used on the object.
Therefore, person1 Sayname() outputs "Nicholas" and person2 Sayname() outputs "Greg".
Remember that sayName() still exists only on person1 and is inherited by person2.

In this example, the inheritance chain of person2 is longer than that of person1.
The person2 object inherits from the person1 object, and the person1 object inherits from the object prototype. See Figure 5-1.

When accessing properties on an object, the JavaScript engine goes through a search process.
If the property is found on the instance (that is, if it is its own property), the property value is used.
If the attribute is not found on the instance, continue searching for [[Prototype]].
If the attribute is not found yet, the search continues to [[Prototype]] of the object Prototype, and so on until the end of the chain is reached.
The chain is usually represented by object At the end of Prototype, its [[Prototype]] is set to null.

You can also use object Create() creates an object of [[Prototype]] whose value is null, for example:

var nakedObject = Object.create(null);

console.log("toString" in nakedObject); // false
console.log("valueOf" in nakedObject); // false

The nakedObject in this example is an object without a prototype chain.
This means that there are no built-in methods such as toString() and valueOf() on the object.
In fact, this object is a completely blank whiteboard without predefined properties, which makes it very suitable for creating lookup hashes without potential naming conflicts with inherited property names.
Objects like this don't have many other uses. You can't use object Use it like prototype.
For example, whenever you use an operator on a nakedObject, you will encounter the error "unable to convert the object to its original value".
However, this is an interesting quirk of the JavaScript language. You can create an object without prototype.

3. Constructor inheritance

Object inheritance in JavaScript is also the basis of constructor inheritance.
Recall from Chapter 4 that almost every function has a prototype property that can be modified or replaced.
The prototype property is automatically assigned as a new generic object, which inherits from object Prototype and has its own attribute called constructor.
In fact, the JavaScript engine does the following for you:

// you write this
function YourConstructor() {
    // initialization
}

// JavaScript engine does this for you behind the scenes
YourConstructor.prototype = Object.create(Object.prototype, {
    constructor: {
        configurable: true,
        enumerable: true,
        value: YourConstructor
        writable: true
    }
});

Therefore, without any additional operation, this code sets the prototype property of the constructor to from object Objects inherited by prototype,
This means that any instance of YourConstructor is also from object Prototype inheritance.
YourConstructor is a subtype of Object, and Object is a supertype of YourConstructor.

Because the prototype attribute is writable, you can change the prototype chain by overriding it. Consider the following example:

function Rectangle(length, width) {
    this.length = length;
    this.width = width;
}

Rectangle.prototype.getArea = function() {
    return this.length * this.width;
};

Rectangle.prototype.toString = function() {
    return "[Rectangle " + this.length + "x" + this.width + "]";
};

// inherits from Rectangle
function Square(size) {
    this.length = size;
    this.width = size;
}

Square.prototype = new Rectangle();
Square.prototype.constructor = Square;

Square.prototype.toString = function() {
    return "[Square " + this.length + "x" + this.width + "]";
};

var rect = new Rectangle(5, 10);
var square = new Square(6);

console.log(rect.getArea()); // 50
console.log(square.getArea()); // 36

console.log(rect.toString()); // "[Rectangle 5x10]"
console.log(square.toString()); // "[Square 6x6]"

console.log(rect instanceof Rectangle); // true
console.log(rect instanceof Object); // true

console.log(square instanceof Square); // true
console.log(square instanceof Rectangle); // true
console.log(square instanceof Object); // true

In this code, there are two constructors: Rectangle and Square.
The prototype property of the Square constructor is overridden with a Rectangle instance.
At this time, no parameters are passed to Rectangle because they do not need to be used. If the arguments are passed, all instances of Square will share the same size.
To change the prototype chain in this way,
You always need to ensure that constructors do not throw errors when no parameters are provided (many constructors contain initialization logic that may require parameters) and that constructors do not change any type of global state, such as tracking the number of instances created.
After overwriting the original value, it will be displayed in square Restore constructor properties on prototype.

After that, rect is created as an instance of Rectangle and Square is created as an instance of Square.
Both objects have a getArea() method because it inherits from rectangle prototype.
The Square variable is considered to be an instance of Square and an instance of Rectangle and Object,
Because instanceof uses the prototype chain to determine the object type. See Figure 5-2.

However, square Prototype does not actually need to be overwritten with a Rectangle object;
The Rectangle constructor does not have any operations required by Square.
In fact, the only relevant part is square Prototype needs to be linked to rectangle in some way Prototype so that inheritance occurs.
This means that you can use object again Create () to simplify this example.

// inherits from Rectangle
function Square(size) {
    this.length = size;
    this.width = size;
}

Square.prototype = Object.create(Rectangle.prototype, {
    constructor: {
        configurable: true,
        enumerable: true,
        value: Square,
        writable: true
    }
});

Square.prototype.toString = function() {
    return "[Square " + this.length + "x" + this.width + "]";
};

In this version of the code, square The prototype is a Rectangle The new object inherited by prototype is overwritten, and there is no need to call the Rectangle constructor.
This means that you don't have to worry about calling the constructor without passing arguments. This code behaves exactly like the previous code.
The prototype chain remains unchanged, so all instances of Square inherit from rectangle Prototype, and the constructor is restored in the same steps.

Always ensure that the prototype is overwritten before adding properties to it, otherwise the added method will be lost in case of overwriting.

4. Constructor theft

Because inheritance is done through the prototype chain in JavaScript, you do not need to call the superclass constructor of the object.
If you really want to call the superclass constructor from the subclass constructor, you need to take advantage of the way the JavaScript function works.

In Chapter 2, you learned about the call() and apply() methods, which allow functions to be called with different this values. This is how constructor stealing works.
You only need to use call() or apply() to call the new created object from the superclass constructor in the subclass constructor.
In fact, you are stealing the superclass constructor of your own object, as shown in the following example:

function Rectangle(length, width) {
    this.length = length;
    this.width = width;
}

Rectangle.prototype.getArea = function() {
    return this.length * this.width;
};

Rectangle.prototype.toString = function() {
    return "[Rectangle " + this.length + "x" + this.width + "]";
};

// inherits from Rectangle
function Square(size) {
    Rectangle.call(this, size, size);
    // optional: add new properties or override existing ones here
}

Square.prototype = Object.create(Rectangle.prototype, {
    constructor: {
        configurable: true,
        enumerable: true,
        value: Square,
        writable: true
    }
});

Square.prototype.toString = function() {
    return "[Square " + this.length + "x" + this.width + "]";
};

var square = new Square(6);

console.log(square.length); // 6
console.log(square.width); // 6
console.log(square.getArea()); // 36

Call the Rectangle constructor in the Square constructor and pass in this and size twice (once length and once width).
Doing so creates the length and width attributes on the new object and makes each attribute equal to size.
This avoids redefining instance properties already defined in the superclass constructor in the subclass constructor.
You can add new properties or overwrite existing properties after applying the superclass constructor.

This two-step process is useful when you need to complete inheritance between custom types. You will always need to modify the prototype of the constructor, and you may also need to call the superclass constructor from the subclass constructor. Typically, you will modify the prototype of method inheritance and steal properties using constructors. This approach is often called pseudo traditional inheritance because it mimics the traditional inheritance of class based languages.

5. Access superclass methods

In the previous example, the Square type has its own toString() method, which masks the toString() on the prototype.
It's quite common to override superclass methods with new features in subclasses, but what if you still want to access supertype methods?
In other languages, you can say super Tostring(), but JavaScript has nothing like that.
Instead, you can directly access the method on the supertype prototype and change the value of this to an instance of a subclass through call() or apply(). For example:

function Rectangle(length, width) {
    this.length = length;
    this.width = width;
}

Rectangle.prototype.getArea = function() {
    return this.length * this.width;
};

Rectangle.prototype.toString = function() {
    return "[Rectangle " + this.length + "x" + this.height + "]";
};

// inherits from Rectangle
function Square(size) {
    Rectangle.call(this, size, size);
}

 Square.prototype = Object.create(Rectangle.prototype, {
    constructor: {
        configurable: true,
        enumerable: true,
        value: Square,
        writable: true
    }
});

// call the supertype method
Square.prototype.toString = function() {
    var text = Rectangle.prototype.toString.call(this);
    return text.replace("Rectangle", "Square");
};

In this version of the code, square prototype. Tostring() calls rectangle. By using call() prototype. toString() .
This method only needs to replace "Rectangle" with "Square" before returning the result text.
This method may seem a bit verbose for such a simple operation, but it is the only way to access supertype methods.

6. Summary

JavaScript supports inheritance through prototype chains.
When an object's [[Prototype]] is set equal to another object, a Prototype chain is created between objects.
All common objects are automatically removed from object Prototype inheritance.
If you want to create an object that inherits from other content, you can use object Create() is a new object and assigns the new object to the [[Prototype]] of the object.

You can complete the inheritance between custom types by creating a prototype chain on the constructor.
By setting the prototype property of the constructor to another value, you can create inheritance between an instance of a custom type and the prototype of that other value.
All instances of this constructor share the same prototype, so they all inherit from the same object.
This technique is ideal for inheriting methods from other objects, but you cannot inherit your own properties using prototypes.

To properly inherit its own properties, you can use constructor stealing, which just calls the constructor with call() or apply() to initialize any subclass objects.
Combining constructor stealing and prototype linking is the most common way to implement inheritance between custom types in JavaScript.
This combination is often called pseudo traditional inheritance because it is similar to inheritance in class based languages.

You can access the methods of the superclass by directly accessing the prototype of the superclass. In doing so, you must use call() or apply() to execute superclass methods on subclass objects.

Keywords: Javascript OOP

Added by Tux-e-do on Fri, 21 Jan 2022 17:44:55 +0200