Object inheritance

An important aspect of object-oriented programming is the inheritance of objects. By inheriting the B object, the A object can directly own all the properties and methods of the B object. This is very useful for code reuse.

Most object-oriented programming languages ​​implement object inheritance through "classes". Traditionally, the inheritance of JavaScript language is not achieved through class, but through "prototype" (prototype). This chapter introduces JavaScript's prototype chain inheritance.

ES6 introduced the class grammar. Class-based inheritance is not introduced in this tutorial. Please refer to the relevant chapters of the book "Introduction to ES6 Standards".

Overview of prototype objects

Disadvantages of constructor

JavaScript generates new objects through constructors, so constructors can be regarded as templates for objects. The properties and methods of the instance object can be defined inside the constructor.

function Cat(name, color) {
  this.name = name;
  this.color = color;
}

var cat1 = new Cat("大毛", "white");

cat1.name; //'big hair'
cat1.color; //'white'

In the above code, the Cat function is a constructor. The name attribute and the color attribute are defined inside the function. All instance objects (the above example is cat1) will generate these two attributes, namely these two attributes Will be defined on the instance object.

Although it is convenient to define properties for instance objects through the constructor, there is a disadvantage. Multiple instances of the same constructor cannot share attributes, resulting in a waste of system resources.

function Cat(name, color) {
  this.name = name;
  this.color = color;
  this.meow = function () {
    console.log("喵喵");
  };
}

var cat1 = new Cat("大毛", "white");
var cat2 = new Cat("Two hairs", "black");

cat1.meow === cat2.meow;
// false

In the above code, cat1 and cat2 are two instances of the same constructor, and they both have the meow method. Since the meow method is generated on each instance object, two instances are generated twice. In other words, every time a new instance is created, a new meow method will be created. This is unnecessary and wastes system resources, because all meow methods have the same behavior and should be shared completely.

The solution to this problem is the prototype of JavaScript.

The role of the prototype property

The design idea of ​​JavaScript inheritance mechanism is that all properties and methods of prototype objects can be shared by instance objects. In other words, if properties and methods are defined on the prototype, then all instance objects can be shared, which not only saves memory, but also reflects the connection between instance objects.

Next, let's see how to specify the prototype for the object. JavaScript stipulates that every function has a prototype property, which points to an object.

function f() {}
typeof f.prototype; // "object"

In the above code, the function f has a prototype property by default, which points to an object.

For ordinary functions, this attribute is basically useless. However, for the constructor, when the instance is generated, the property will automatically become the prototype of the instance object.

function Animal(name) {
  this.name = name;
}
Animal.prototype.color = "white";

var cat1 = new Animal("大毛");
var cat2 = new Animal("Er Mao");

cat1.color; //'white'
cat2.color; //'white'

In the above code, the prototype property of the constructor Animal is the prototype object of the instance objects cat1 and cat2. A color property is added to the prototype object, and as a result, the instance objects all share this property.

The properties of the prototype object are not properties of the instance object itself. As long as the prototype object is modified, the change will immediately be reflected in all instance objects.

Animal.prototype.color = "yellow";

cat1.color; // "yellow"
cat2.color; // "yellow"

In the above code, the value of the color property of the prototype object becomes yellow, and the color property of the two instance objects immediately changes accordingly. This is because the instance object does not actually have a color property, and instead reads the color property of the prototype object. In other words, when the instance object itself does not have a certain property or method, it will go to the prototype object to find the property or method. This is what makes prototype objects special.

If the instance object itself has a certain property or method, it will not go to the prototype object to find this property or method.

cat1.color = "black";

cat1.color; //'black'
cat2.color; //'yellow'
Animal.prototype.color; //'yellow';

In the above code, the color property of the instance object cat1 is changed to black, which makes it no longer go to the prototype object to read the color property, and the value of the latter is still yellow.

To sum up, the role of the prototype object is to define the properties and methods shared by all instance objects. This is why it is called a prototype object, and an instance object can be regarded as a sub-object derived from the prototype object.

Animal.prototype.walk = function () {
  console.log(this.name + "is walking");
};

In the above code, a walk method is defined on the Animal.prototype object, and this method can be called on all Animal instance objects.

Prototype chain

JavaScript stipulates that all objects have their own prototype object (prototype). On the one hand, any object can serve as the prototype of other objects; on the other hand, since the prototype object is also an object, it also has its own prototype. Therefore, a "prototype chain" will be formed: the prototype of the object to the prototype, and then to the prototype...

If you trace back layer by layer, the prototypes of all objects can eventually be traced back to Object.prototype, which is the prototype property of the Object constructor. In other words, all objects inherit the properties of Object.prototype. This is why all objects have valueOf and toString methods, because this is inherited from Object.prototype.

So, does the Object.prototype object have its prototype? The answer is that the prototype of Object.prototype is null. null does not have any properties or methods, nor does it have its own prototype. Therefore, the end of the prototype chain is null.

Object.getPrototypeOf(Object.prototype);
// null

The above code indicates that the prototype of the Object.prototype object is null. Since null has no attributes, the prototype chain ends here. The Object.getPrototypeOf method returns the prototype of the parameter object. For details, please see the following text.

When reading a certain property of an object, the JavaScript engine first looks for the property of the object itself. If it can't find it, it goes to its prototype. If it still can't find it, it goes to the prototype of the prototype. If the topmost Object.prototype is still not found, then undefined is returned. If both the object itself and its prototype define an attribute with the same name, then the attributes of the object itself will be read first. This is called "overriding".

Note that looking for a certain attribute in the entire prototype chain has an impact on performance. The prototype object whose properties are in the upper layer has a greater impact on performance. If you look for a non-existent property, the entire prototype chain will be traversed.

For example, if the prototype property of the constructor points to an array, it means that the instance object can call the array method.

var MyArray = function () {};

MyArray.prototype = new Array();
MyArray.prototype.constructor = MyArray;

var mine = new MyArray();
mine.push(1, 2, 3);
mine.length; // 3
mine instanceof Array; // true

In the above code, mine is the instance object of the constructor function MyArray, because MyArray.prototype points to an array instance, so that mine can call array methods (these methods are defined on the prototype object of the array instance) . The last line of the instanceof expression is used to compare whether an object is an instance of a certain constructor. The result is to prove that mine is an instance of Array. The detailed explanation of the instanceof operator is detailed below.

The above code also shows the constructor property of the prototype object. The meaning of this property will be explained in the next section.

constructor property

The prototype object has a constructor property, which by default points to the constructor where the prototype object is located.

function P() {}
P.prototype.constructor === P; // true

Since the constructor attribute is defined on the prototype object, it means that it can be inherited by all instance objects.

function P() {}
var p = new P();

p.constructor === P; // true
p.constructor === P.prototype.constructor; // true
p.hasOwnProperty("constructor"); // false

In the above code, p is the instance object of the constructor P, but p itself does not have the constructor property, which actually reads the P.prototype.constructor property on the prototype chain.

The function of the constructor attribute is to know which constructor produced an instance object.

function F() {}
var f = new F();

f.constructor === F; // true
f.constructor === RegExp; // false

In the above code, the constructor attribute determines that the constructor of the instance object f is F instead of RegExp.

On the other hand, with the constructor property, you can create another instance from one instance object.

function Constr() {}
var x = new Constr();

var y = new x.constructor();
y instanceof Constr; // true

In the above code, x is an instance of the constructor Constr, which can be called indirectly from x.constructor. This makes it possible to call its own constructor in an instance method.

Constr.prototype.createCopy = function () {
  return new this.constructor();
};

In the above code, the createCopy method calls the constructor to create another instance.

The constructor attribute represents the relationship between the prototype object and the constructor. If the prototype object is modified, the constructor attribute will generally be modified at the same time to prevent errors during reference.

function Person(name) {
  this.name = name;
}

Person.prototype.constructor === Person; // true

Person.prototype = {
  method: function () {},
};

Person.prototype.constructor === Person; // false
Person.prototype.constructor === Object; // true

In the above code, the prototype object of the constructor Person is changed, but the constructor property is not modified, which causes this property to no longer point to the Person. Because the new prototype of Person is an ordinary object, and the constructor property of the ordinary object points to the Object constructor, resulting in Person.prototype.constructor becoming Object.

Therefore, when modifying the prototype object, it is generally necessary to modify the pointer of the constructor attribute at the same time.

// bad wording
C.prototype = {
  method1: function (...) {... },
  // ...
};

// good writing
C.prototype = {
  constructor: C,
  method1: function (...) {... },
  // ...
};

// better writing
C.prototype.method1 = function (...) {... };

In the above code, either re-point the constructor property to the original constructor, or only add a method to the prototype object, so as to ensure that the instanceof operator will not be distorted.

If you are not sure what function the constructor attribute is, there is another way: get the name of the constructor from the instance through the name attribute.

function Foo() {}
var f = new Foo();
f.constructor.name; // "Foo"

instanceof operator

The instanceof operator returns a boolean value indicating whether the object is an instance of a certain constructor.

var v = new Vehicle();
v instanceof Vehicle; // true

In the above code, the object v is an instance of the constructor Vehicle, so it returns true.

The left side of the instanceof operator is the instance object, and the right side is the constructor. It checks whether the prototype object of the constructor on the right is on the prototype chain of the object on the left. Therefore, the following two ways of writing are equivalent.

v instanceof Vehicle;
// Equivalent to
Vehicle.prototype.isPrototypeOf(v);

In the above code, Vehicle is the constructor of the object v, its prototype object is Vehicle.prototype, and the isPrototypeOf() method is a native method provided by JavaScript to check whether an object is another The prototype of the object is explained in detail later.

Since instanceof checks the entire prototype chain, the same instance object may return true for multiple constructors.

var d = new Date();
d instanceof Date; // true
d instanceof Object; // true

In the above code, d is an instance of both Date and Object, so it returns true for both constructors.

Since any object (except null) is an instance of Object, the instanceof operator can determine whether a value is an object other than null.

var obj = { foo: 123 };
obj instanceof Object; // true

null instanceof Object; // false

In the above code, except for null, the operation result of instanceOf Object of other objects is true.

The principle of instanceof is to check whether the prototype property of the constructor on the right is on the prototype chain of the object on the left. There is a special case, that is, there are only null objects in the prototype chain of the object on the left. At this time, the instanceof judgment will be distorted.

var obj = Object.create(null);
typeof obj; // "object"
obj instanceof Object; // false

In the above code, Object.create(null) returns a new object obj whose prototype is null (for details of Object.create(), see later). The prototype property of the constructor function Object on the right is not on the prototype chain on the left, so instanceof considers obj not an instance of Object. This is the only case where the instanceof operator's judgment will be distorted (the prototype of an object is null).

One use of the instanceof operator is to determine the type of value.

var x = [1, 2, 3];
var y = {};
x instanceof Array; // true
y instanceof Object; // true

In the above code, the instanceof operator judges that the variable x is an array, and the variable y is an object.

Note that the instanceof operator can only be used on objects, not primitive values.

var s = "hello";
s instanceof String; // false

In the above code, the string is not an instance of the String object (because the string is not an object), so false is returned.

In addition, for undefined and null, the instanceof operator always returns false.

undefined instanceof Object; // false
null instanceof Object; // false

Using the instanceof operator, you can also cleverly solve the problem of forgetting to add the new command when calling the constructor.

function Fubar(foo, bar) {
  if (this instanceof Fubar) {
    this._foo = foo;
    this._bar = bar;
  } else {
    return new Fubar(foo, bar);
  }
}

The above code uses the instanceof operator to determine whether the this keyword is an instance of the constructor Fubar inside the function body. If it is not, it means that you forgot to add the new command.

Constructor inheritance

It is a very common requirement to let one constructor inherit another constructor. This can be achieved in two steps. The first step is to call the constructor of the parent class in the constructor of the subclass.

function Sub(value) {
  Super.call(this);
  this.prop = value;
}

In the above code, Sub is the constructor of the subclass, and this is the instance of the subclass. Calling the super class's constructor function on the instance will let the subclass instance have the properties of the super class instance.

The second step is to make the prototype of the subclass point to the prototype of the parent class, so that the subclass can inherit the prototype of the parent class.

Sub.prototype = Object.create(Super.prototype);
Sub.prototype.constructor = Sub;
Sub.prototype.method = "...";

In the above code, Sub.prototype is the prototype of the subclass. It should be assigned to Object.create(Super.prototype) instead of being directly equal to Super.prototype. Otherwise, the operations on Sub.prototype in the next two lines will be modified together with the prototype Super.prototype of the parent class.

Another way of writing is that Sub.prototype is equal to an instance of the parent class.

Sub.prototype = new Super();

The above wording also has the effect of inheritance, but the child class will have the method of the parent class instance. Sometimes, this may not be what we need, so this way of writing is not recommended.

For example, the following is a Shape constructor.

function Shape() {
  this.x = 0;
  this.y = 0;
}

Shape.prototype.move = function (x, y) {
  this.x += x;
  this.y += y;
  console.info("Shape moved.");
};

We need to make the Rectangle constructor inherit from Shape.

// In the first step, the child class inherits the instance of the parent class
function Rectangle() {
  Shape.call(this); // call the parent class constructor
}
// Another way of writing
function Rectangle() {
  this.base = Shape;
  this.base();
}

// In the second step, the child class inherits the prototype of the parent class
Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;

In this way, the instanceof operator will return true to the constructors of the subclass and the parent class.

var rect = new Rectangle();

rect instanceof Rectangle; // true
rect instanceof Shape; // true

In the above code, the child class inherits the parent class as a whole. Sometimes only the inheritance of a single method is needed. At this time, the following wording can be used.

ClassB.prototype.print = function () {
  ClassA.prototype.print.call(this);
  // some code
};

In the above code, the print method of the subclass B first calls the print method of the parent class A, and then deploys its own code. This is equivalent to inheriting the print method of the parent class A.

Multiple inheritance

JavaScript does not provide multiple inheritance functions, that is, one object is not allowed to inherit multiple objects at the same time. However, this function can be achieved through workarounds.

function M1() {
  this.hello = "hello";
}

function M2() {
  this.world = "world";
}

function S() {
  M1.call(this);
  M2.call(this);
}

// inherit M1
S.prototype = Object.create(M1.prototype);
// Join M2 on the inheritance chain
Object.assign(S.prototype, M2.prototype);

// Specify the constructor
S.prototype.constructor = S;

var s = new S();
s.hello; //'hello'
s.world; //'world'

In the above code, the subclass S inherits both the parent classes M1 and M2. This mode is also called Mixin.

Module

As websites gradually become "Internet applications," the JavaScript code embedded in web pages becomes larger and more complex. Web pages are more and more like desktop programs, requiring a team of division of labor, schedule management, unit testing, etc... Developers must use software engineering methods to manage the business logic of the web.

JavaScript modular programming has become an urgent need. Ideally, developers only need to implement the core business logic, and everything else can load modules already written by others.

However, JavaScript is not a modular programming language, ES6 only began to support "classes" and "modules." The following describes the traditional approach, how to use objects to achieve the effect of the module.

Basic implementation method

A module is an encapsulation of a set of attributes and methods that implement specific functions.

The simple way is to write the module as an object, and all module members are placed in this object.

var module1 = new Object({
  _count: 0,
  m1: function () {
    //...
  },
  m2: function () {
    //...
  },
});

The above functions m1 and m2 are all encapsulated in the module1 object. When it is used, the properties of this object are called.

module1.m1();

However, this way of writing will expose all module members, and the internal state can be rewritten externally. For example, external code can directly change the value of the internal counter.

module1._count = 5;

Encapsulation of private variables: how to write a constructor

We can use the constructor to encapsulate private variables.

function StringBuilder() {
  var buffer = [];

  this.add = function (str) {
    buffer.push(str);
  };

  this.toString = function () {
    return buffer.join("");
  };
}

In the above code, buffer is a private variable of the module. Once the instance object is generated, the buffer cannot be accessed directly from the outside. However, this method encapsulates the private variables in the constructor, resulting in the constructor and the instance object being integrated, always exist in the memory, and cannot be cleared after use. This means that the constructor has a dual role, not only to shape the instance object, but also to save the data of the instance object, which violates the principle of data separation between the constructor and the instance object (that is, the data of the instance object should not be saved Outside the instance object). At the same time, it is very memory intensive.

function StringBuilder() {
  this._buffer = [];
}

StringBuilder.prototype = {
  constructor: StringBuilder,
  add: function (str) {
    this._buffer.push(str);
  },
  toString: function () {
    return this._buffer.join("");
  },
};

This method puts private variables into the instance object. The advantage is that it looks more natural, but its private variables can be read and written from the outside, which is not very safe.

Encapsulation of private variables: the way to execute functions immediately

Another approach is to use "Immediately-Invoked Function Expression" (IIFE) to encapsulate related properties and methods in a function scope, which can achieve the purpose of not exposing private members.

var module1 = (function () {
  var _count = 0;
  var m1 = function () {
    //...
  };
  var m2 = function () {
    //...
  };
  return {
    m1: m1,
    m2: m2,
  };
})();

Using the above writing method, external code cannot read the internal _count variable.

console.info(module1._count); //undefined

The above module1 is the basic way of writing JavaScript modules. Next, we will process this writing method.

Module zoom mode

If a module is very large and must be divided into several parts, or one module needs to inherit from another module, then it is necessary to adopt the "augmentation mode".

var module1 = (function (mod) {
  mod.m3 = function () {
    //...
  };
  return mod;
})(module1);

The above code adds a new method m3() to the module1 module, and then returns the new module1 module.

In the browser environment, each part of the module is usually obtained from the Internet, and sometimes it is impossible to know which part will be loaded first. If the above writing method is adopted, the first execution part may load an empty object that does not exist, and then the "wide enlargement mode" (Loose augmentation) must be used.

var module1 = (function (mod) {
  //...
  return mod;
})(window.module1 || {});

Compared with "magnification mode", "wide magnification mode" means that the parameter of "execute function immediately" can be an empty object.

Enter global variables

Independence is an important feature of a module, and it is best not to directly interact with other parts of the program within the module.

In order to call global variables within the module, other variables must be explicitly entered into the module.

var module1 = (function ($, YAHOO) {
  //...
})(jQuery, YAHOO);

The above module1 module needs to use the jQuery library and the YUI library, so use these two libraries (in fact, two modules) as parameters and enter module1. In addition to ensuring the independence of the modules, this also makes the dependencies between the modules obvious.

Immediately executing functions can also act as a namespace.

(function ($, window, document) {
  function go(num) {}

  function handleEvents() {}

  function initialize() {}

  function dieCarouselDie() {}

  //attach to the global scope
  window.finalCarousel = {
    init: initialize,
    destroy: dieCarouselDie,
  };
})(jQuery, window, document);

In the above code, the finalCarousel object is output to the global, and the init and destroy interfaces are exposed to the outside. The internal methods go, handleEvents, initialize, and dieCarouselDie are all externally callable.