Advanced JavaScript Programming (3rd Edition) Reading Notes Chapter 6 Object-Oriented Programming

Keywords: Javascript Attribute ECMAScript IE

  • Object-Oriented (OO) languages have a flag that they all have the concept of a class through which any number of objects with the same properties and methods can be created.
  • There is no class concept in ECMAScript, so its objects are different from those in the language of the base class.
  • ECMA-262 defines an object as "a collection of disordered attributes whose attributes can contain basic values, objects, or functions."We can think of an ECMAScript object as a hash list: nothing more than a set of name-value pairs, where values can be data or functions
  • Each object is created based on a reference type, either native or developer-defined, as discussed in Chapter 5

Understanding Objects

// Create objects and assign attributes
var person = new Object();
person.name = "Nicholas";
person.age = 29;
person.job = "Sofware Engineer";

person.sayName = function() {
  alert(this.name);
}

// Create objects literally
var person = {
  name: "Nicholas",
  age: 29,
  job: "Sofware Engineer",
  sayName: function() {
    alert(this.name);
  }
}

Attribute type

  • There are two properties in ECMAScript: data properties and accessor properties

Data Properties

  • The data attribute contains the position of a numeric value.Values can be read and written at this location.Data attributes have four characteristics that describe their behavior

    • [[Configurable]] Indicates whether attributes can be redefined by deleting them, whether attributes can be modified, or whether attributes can be modified as accessor attributes.Properties that are directly defined in the object as in the preceding example, and their default value is true
    • [[Enumerable]] Indicates whether attributes can be returned through a for-in loop.Previous examples This feature defaults to true
    • [[Writable]] Indicates whether the value of an attribute can be modified.Previous examples This feature defaults to true
    • [[Value]] Contains the data value of this property.Read attributes are affordable, read from this location; write attributes are affordable, save new values in this location.The default value for this feature is undefined
  • To modify the default attributes of attributes, you must use the Object.defineProperty() method of ECMAScript5.

    • There are three main parameters: the object where the property is located, the name of the property, and a descriptor object.The properties of a descriptor object must be: configurable,enumerable,writalbe, and value.
    • Set one or more of these values to modify the corresponding attribute values
    var person = {};
    // A name attribute was created, and its value "Nicholas" is read-only.
    Object.defineProperty(person, "name", {
      writable: false,
      value: "Nicholas"
    });
    
    console.log(person.name);                // "Nicholas"
    // Assignment is ignored in non-strict mode
    // In strict mode, this results in errors
    person.name = "Greg";
    console.log(person.name);                // "Nicholas"
    • Setting configurable to false means that properties cannot be deleted from the object.If delete is called on this property, nothing will happen in non-strict mode, and errors will result in strict mode
    • Once a property is defined as unconfigurable, it can no longer be made configurable.Calling the Object.defineProperty() method again at this time to modify attributes other than writable will result in an error
    var person = {};
    Object.defineProperty(person, "name", {
      configurable: false,
      value: "Nicholas"
    });
    
    // Throw an error
    Object.defineProperty(person, "name", {
      configurable: true,
      value: "Nicholas"
    })
    • The Object.defineProperty() method can be called multiple times to modify the same property, but this is limited when the configurable property is set to false.
    • Calling Object.defineProperty() If not specified, the default values for configurable, enumerable, and writalbe attributes are false.In most cases, it is not necessary to take advantage of the advanced functionality provided by the Object.defineProperty() method.

Accessor Properties

  • Accessor properties do not contain data values, they contain a pair of getter s and setter functions (not required)
  • When reading accessor properties, the getter function is called, which is responsible for returning valid values
  • When an accessor property is written, the setter function is called and a new value is passed in, which is responsible for deciding how to process the data
  • Accessor properties have the following four features:

    • [[Configurable]] indicates whether attributes can be redefined by deleting them, whether attributes can be modified, or whether attributes can be modified as data attributes.The default value of this property is true for properties defined directly on the object
    • [[Enumerable]] Indicates whether attributes can be returned through a for-in loop.The default value of this property is true for properties defined directly on the object
    • [[Get]] Function called when reading a property.Default value is undefined
    • [[Set]] Function that is called when a property is written.Default value is undefined
  • Accessor properties cannot be directly defined and must be defined using Object.defineProperty()
var book = {
  _year: 2004,
  edition: 1
};

Object.defineProperty(book, "year", {
  get: function () {
    return this._year;
  },
  set: function (newValue) {
    if (newValue > 2004) {
      this._year = newValue;
      this.edition += newValue - 2004;
    }
  }
});

// This is a common way to use accessor properties, where setting the value of one property causes other properties to change.
book.year = 2005;
console.log(book.edition);   // 2
  • You don't have to make both getters and setter s.Specifying only getters means that attributes cannot be written, and attempts to write attributes are ignored.Attempting to write attributes that specify only the getter function in strict mode throws an error.
  • Specifying only setter means that properties cannot be read, otherwise undefined is returned in non-strict mode and errors are thrown in strict mode
  • Browsers that support ECMAScript 5 include IE9+ (IE8 is only partially implemented), Firefox 4+, Safari 5+, Opera 12+, Chrome
  • Cannot modify [[Configurable]] and [[Enumerable]] in browsers that do not support the Object.defineProperty() method

Define multiple attributes

  • Object.defineProperties() Method for defining multiple attributes
var book = {}
Object.defineProperties(book, {
  _year: {
    writable: true,
    value: 2004
  },

  edition: {
    writable: true,
    value: 1
  },

  year: {
    get: function() {
      return this._year;
    },

    set: function(newValue) {
      if (newValue > 2004) {
        this._year = newValue;
        this.editio += newValue - 2004;
      }
    }
  }
})

Features of reading attributes

  • The Ojbect.getOwnPropertyDescriptor() method, which takes a descriptor for a given property, takes two parameters: the object the property is in and the name of the property to be read from.When returning a value, an object whose properties are configurable, enumerable, get, set if it is an accessor property; if it is a data property, the object's properties are configurable, enumerable, writable, value
var book = {};

Object.defineProperties(book, {
  _year: {
    writable: true,
    value: 2004
  },

  edition: {
    writable: true,
    value: 1
  },

  year: {
    get: function() {
      return this._year;
    },

    set: function(newValue) {
      if (newValue > 2004) {
        this._year = newValue;
        this.editio += newValue - 2004;
      }
    }
  }
});

var descriptor = Object.getOwnPropertyDescriptor(book, "_year");
console.log(descriptor.value);                                    // 2004
console.log(descriptor.configurable);                             // false
console.log(typeof descriptor.get);                               // undefined

var descriptor = Object.getOwnPropertyDescriptor(book, "year");
console.log(descriptor.value);                                    // undefined
console.log(descriptor.enumerable);                               // false
console.log(typeof descriptor.get);                               // "function"

create object

  • Although Object constructors or object literals can be used to create a single object, they have one obvious disadvantage: creating many objects with the same interface can result in a lot of duplicate code.To solve this problem, people began to use a variant of factory mode

Factory Mode

  • Given that classes cannot be created in ECMAScript, developers have invented a function that encapsulates the details of creating objects with a specific interface.
function createPerson(name, age, job) {
  var o = new Object();
  o.name = name;
  o.age = age;
  o.job = job;
  o.sayName = function () {
    alert(this.name);
  };

  return o;
}

var person1 = createPerson("Nicholas", 29, "Software Engineer");
var person2 = createPerson("Greg", 27, "Doctor");
  • Although factory mode solves the problem of creating multiple similar objects, it does not solve the problem of object recognition (that is, how to know the type of an object).

Constructor Pattern

  • Custom constructors can be created to define properties and methods for custom object types
function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = function () {
    alert(this.name);
  };
}

var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
  • The constructor pattern differs from the factory pattern in the following ways

    • No explicit creation object
    • Assign attributes and methods directly to this object
    • No return statement
    • Function names are capitalized, and by convention, constructors should start with a capital letter, not a constructor lowercase
  • Personon1 and person2 hold a different instance of Person.Both have a constructor property that changes the property to point to Person
console.log(person1.constructor == Person); // true
console.log(person2.constructor == Person); // true
  • The constructor property of the object was originally used to identify the object type.However, it is more reliable to detect object types or instanceof operators.
  • All objects created in this example are instances of both Object and Person
console.log(person1 instanceof Object);   // true
console.log(person2 instanceof Object);   // true
console.log(person1 instanceof Person);   // true
console.log(person2 instanceof Person);   // true
  • Creating a custom constructor means that its instances can be identified as a specific type in the future, which is where building a Korean pattern outweighs a factory one.In this example, person1 and person2 are both instances of Object because all objects inherit from Object
  • Constructions defined in this way are defined in Global objects (window s in browsers).Chapter 8 will discuss the Browser Object Model (BOM) in detail

Treat the constructor as a function

  • The only difference between constructors and other functions is the way they are called.
  • Constructors are also functions, and there is no special syntax for defining constructors
  • Any function that is called by the new operator can be used as a constructor
  • And any function is not called by the new operator, so it's no different from a normal function
// Use as a constructor
var person = new Person("Nicholas", 29, "Software Engineer");
person.sayName();   // "Nicholas"

// As a normal function call, this points to the window object
Person("Greg", 27, "Doctor");  // Add to window object
window.sayName();       // "Greg"

// Called in the scope of another object
var o = new Object();
Person.call(o, "Kristen", 25, "Nurse");
o.sayName(); // "Kristen"

Problems with Constructors

  • The constructor pattern is not without drawbacks.The main problem is that each method needs to be recreated on each instance.In the previous example, person1 and person2 both have a method named sayName(), but the two methods are not instances of the same Function.Don't forget that functions are objects in ECMAScript, so each function you define instantiates an object.Logically, the constructor can also be defined like this
function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  // Equivalent to declared function on Subgrade
  this.sayName = new Function("alert(this.name)");
}

console.log(person1.sayName == person2.sayName);  // false
  • With this object, there is no need to bind a function to a specific object before executing code, simplifying by transferring the function definition outside the function
function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  this.sayName = sayName
}

function sayName () {
  alert(this.name);
}

var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
  • We moved the definition of the sayName function outside of the function.Inside the constructor, we set the sayName property to equal the global sayName function.Thus, since sayName contains a pointer to a function, the person1 and person2 objects share the same sayName() function defined in the global scope.
  • A new problem arises where a function defined in a global scope can actually only be called by an object, which makes the global scope a bit misnamed.What's more unacceptable is that if an object needs to define many methods, then it needs to define many global functions, so our custom reference type is not encapsulated at all.
  • The good thing is that you can solve this by prototyping

Prototype mode

  • Each function we create has a prototype, which is a pointer to an object whose purpose is to contain properties and methods that can be shared by all instances of a particular type.
  • Prototype is the prototype object of that object instance created by calling the constructor. The advantage of using a prototype object is that all object instances can share the properties and methods it contains.In other words, instead of defining object instance information in the constructor, you can add that information directly to the prototype object
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function () {
  alert(this.name);
};

var person1 = new Person();
person1.sayName();    // "Nicholas"

var person2 = new Person();
person2.sayName();    // "Nicholas"

console.log(person1.sayName == person2.sayName);    // true
  • Here, we add the sayName() method and all properties directly to Person's prototype property, and the constructor becomes empty.Even so, new objects can still be created by calling constructors, and they will have the same properties and methods.
  • However, unlike the constructor pattern, these properties and methods of the new object are shared by all instances.In other words, person1 and person2 access the same set of properties and methods
  • To understand how prototype schemas work, you must first understand the nature of prototype objects in ECMAScript

Understanding prototype objects

  • Whenever a function is created, a prototype property is created for the function based on a specific set of rules, which points to the prototype object of the function.
  • By default, all prototype objects automatically get a constructor property, which is a pointer to the function in which the prototype property resides.For example, Person.prototype.constructor points to Person.
  • We can continue to add additional properties and methods to the prototype object
  • Once a custom constructor is created, its prototype object only gets the constructor property by default, and all other methods inherit from Object.When a constructor is called to create a new instance, the inside of the instance will contain a pointer (internal property) to the prototype object of the constructor.This is called [[Prototype]] in ECMAScript 5.
  • Although there is no standard way to access [[Prototype]] in scripts, Firefox, Safari, Chrome supports a property u proto_u on each object; this property is completely invisible to scripts.
  • This link exists between the instance and the prototype object of the constructor, not between the instance and the constructor

  • Figure 6-1 (page 148) shows the Person constructor, the prototype properties of Person, and the relationship between two existing instances of Person.Here, Person.prototype points to the prototype object, and Person.prototype.constructor points back to Person.In addition to the constructor property, the prototype object includes other properties that were added later.Each instance of Person, person1 and person2, contains an internal attribute that only points to Person.prototype; in other words, person1 and person2 have no direct relationship to the constructor.Also, it is important to note that although neither instance contains attributes or methods, we can call person1.sayName().This is achieved through the process of finding object properties.
  • Although [Prototype]] is not accessible in all implementations, the isPrototypeOf() method can be used to determine whether this relationship exists between objects.
console.log(Person.prototype.isPrototypeOf(person1));     // true
console.log(Person.prototype.isPrototypeOf(person2));     // true

console.log(Person.isPrototypeOf(person1));     // false
console.log(Person.isPrototypeOf(person2));     // false
  • ECMAScript 5 adds a new method, Object.getPrototypeOf(), which returns the value of [[Prototype]] in all supported implementations.Supported browsers IE9+, Safari 5+, Opera 12+, Chrome
console.log(Object.getPrototypeOf(person1) == Person.prototype); // true
console.log(Object.getPrototypeOf(person1).name);                // "Nicholas"
  • Each time the code reads an attribute of an object, a search is performed to target an attribute with a given name.The search starts with the object instance itself, and if it is not found, continues to search for the prototype object pointed to by the pointer, looking for attributes with the given name in the prototype object.That is, when we call person1.sayName(), we perform two searches first, find no sayName attribute in person1, continue searching in person1's prototype, find the sayName attribute in Person.prototype, and read the function stored in the prototype object.
  • The prototype initially contained only the constructor property, which is also shared and therefore accessible through an object instance
  • Although values stored in the prototype can be accessed through object instances, values in the prototype cannot be overridden through object instances.If we add an attribute to an instance with the same name as an attribute in the instance prototype, then we create the attribute in the instance, which will mask that attribute in the prototype
  • The hasOwnProperty() method (inherited from Object) can detect whether an attribute exists in an instance or in a prototype.
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function () {
  alert(this.name);
};

var person1 = new Person();
var person2 = new Person();

console.log(person1.hasOwnProperty("name"));       // false

person1.name = "Greg";
console.log(person1.name);                         // "Greg" - from an instance
console.log(person1.hasOwnProperty("name"));       // true

console.log(person2.name);                         // "Nicholas" - From Prototype
console.log(person2.hasOwnProperty("name"));       // false

// Delete attributes from an instance using the delete operator
delete person1.name;
console.log(person1.name);                         // "Nicholas" - from an instance
console.log(person1.hasOwnProperty("name"));       // false
  • The Object.getOwnPropertyDescriptor() method of ECMAScript 5 can only be used for instance properties. To get a descriptor of a prototype property, you must call the Object.getOwnPropertyDescriptor() method directly on the prototype object

Prototype and in operator

  • The in operator is used in both ways, individually, and in a for-in loop.
  • Using the in operator alone returns true when a given property is accessible through an object, whether it exists in an instance or in a prototype.
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function () {
  alert(this.name);
};

var person1 = new Person();
var person2 = new Person();

console.log(person1.hasOwnProperty("name"));       // false
console.log("name" in person1);                    // true

person1.name = "Greg";
console.log("name" in person1);                    // true

console.log(person2.name);                         // "Nicholas" - From Prototype
console.log("name" in person2);                    // true

// Delete attributes from an instance using the delete operator
delete person1.name;
console.log("name" in person1);                    // true
  • Using both hasOwnProperty() method and in operator, you can determine whether the property exists in the instance or in operation
function hasPrototypeProperty(object, name) {
  return !object.hasOwnProperty(name) && (name in object);
}
  • Since the in operator returns true as long as the property is accessible and hasOwnproperty() only returns true if the property exists in the instance, as long as the in operator returns true and hasOwnproperty() returns false, you can determine that the property is a property in the prototype.
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function () {
  alert(this.name);
};

var person = new Person();
console.log(hasPrototypeProperty(person, "name"));      // true

person.name = "Greg";
console.log(hasPrototypeProperty(person, "name"));      // false
  • Using a for-in loop, all enumerad properties that can be accessed through an object are returned, including both those that exist in the instance and those that exist in the prototype.Instance attributes that block non-enumerable attributes in the prototype (i.e., attributes that [[Enumerable]] is marked as false) are also returned in the for-in loop because all developer-defined attributes are enumerable by rule - except in IE8 and earlier versions.
  • There was a bug in earlier versions of IE implementations that instance properties that shield non-enumerable properties would not appear in the for-in loop.
var o = {
  toString: function() {
    return  "My Object";
  }
};

for (var prop in o) {
  if (prop ==  "toString") {
    console.log("Found toString");    // Will not be displayed in IE
  }
}
  • In IE, because its implementation considers the prototype toString() method marked with a value of false [[Enumerable]]], the property should be skipped and the result will not be printed.This bug affects all properties and methods that are not enumerable by default, including hasOwnProperty(), propertyIsEnumerable(), toLocaleString(), valueOf()
  • ECMAScript 5 also sets the [Enumerable]] attribute of the constructor and prototype properties to false, but not all browsers do.
  • To get all enumerable instance properties on an object, you can use the Object.keys() method of ECMAScript5.This method takes an object as a parameter and returns an array of strings containing all enumerable properties
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "SoftWare Engineer";
Person.prototype.sayName = function() {
  console.log(this.name);
};

var keys = object.keys(Person.prototype);
console.log(keys);                            // "name,age,job,sayName"

var p1 = new Person();
p1.name = "Rob";
p1.age = 31;
var p1kyes = Object.keys(p1);
console.log(p1kyes);                         // "name,age"
  • If you want to get all the instance properties, whether they are enumerable or not, you can use the Object.getOwnPropertyNames() method.
var keys = Object.getOwnPropertyNames(Person.prototype);     // "constructor,name,age,job,sayName"
  • Note that the result contains a non-enumerable constructor attribute.Both the Object.keys() and Object.getOwnPropertyNames() methods can be used to replace the for-in loop.Browsers that support both methods are IE9+, Firefox4+, Safari5+, Opera12+, Chrome

Simpler prototype syntax

  • In the previous example, you typed Person.prototype once for each property and method you added.To reduce unnecessary input and to better visually encapsulate the capabilities of a prototype, it is more common to rewrite the entire prototype object with a literal amount of an object containing all the properties and methods
function Person() {}

Person.prototype = {
  name: "Nicholas",
  age: 29,
  job: "Software Engineer",
  sayName: function () {
    console.log(this.name);
  }
};
  • We set Person.prototype equal to a new object created as an object literal.The end result is the same, with one exception: the constructor property no longer points to Person.Because every time a function is created, its prototype object is created, and the entire object automatically gets the constructor property.The syntax we use here essentially completely overrides the default prototype object, so the constructor property becomes the constructor property of the new object (pointing to the Object constructor) and no longer to the Person function.At this point, although the instanceof operator can return the correct results, the type of object cannot be determined by the constructor
var friend = new Person();

console.log(friend instanceof Object);                     // true
console.log(friend instanceof Person);                     // true
console.log(friend.constructor == Person);                 // false
console.log(friend.constructor == Object);                 // Object
  • If the value of the constructor is really important, you can deliberately set it back to the appropriate value as follows.
function Person() {}

Person.prototype = {
  constructor: Person,                          // Redirect prototype constructor to Person
  name: "Nicholas",
  age: 29,
  job: "Software Engineer",
  sayName: function () {
    console.log(this.name);
  }
};
  • Resetting the constructor in this way will cause its [[Enumerable]] attribute to be set to true.By default, the native constructor attribute is not enumerable, so if you are using a JavaScript engine compatible with ECMAScript 5, you can try Object.defineProperty()
function Person() {}

Person.prototype = {
  name: "Nicholas",
  age: 29,
  job: "Software Engineer",
  sayName: function () {
    console.log(this.name);
  }
};

// Reset constructor, only for ECMASCript 5 compatible browsers
Object.defineProperty(Person.prototype, "constructor", {
  enumerable: false,
  value: Person
});

Dynamics of prototypes

  • Since the process of finding values in a prototype is a search, any modifications we make to the prototype object are immediately reflected in the instance -- even if the instance was created before the prototype was modified
var friend = new Person();

Person.prototype.sayHi = function() {
  console.log("Hi");
};

friend.sayHi();               // "Hi"
  • Although attributes and methods can be added to a prototype at any time, and modifications can be reflected immediately in all object instances, the situation is different if the entire prototype object is rewritten.Calling a constructor adds a [[Protoype]] pointer to the original prototype for the instance, and modifying the prototype to another object cuts off the connection between the constructor and the original prototype.Keep in mind that the pointer in the instance only points to the prototype, not to the constructor.
function Person() {}

var friend = new Person();

// Rewriting an entire prototype object severs the connection between the constructor and the original prototype
Person.prototype = {
  constructor: Person,
  age: 29,
  job: "Software Engineer",
  sayName: function () {
    console.log(this.name);
  }
};

friend.sayName();  // error

Prototype of native object

  • The importance of prototype patterns is not only in creating custom types, but also in creating all native reference types.All native reference types (Object, Array, String, and so on) define methods on the prototype of their constructors.For example, the sort() method can be found in Array.prototype, and the substring() method can be found in String.prototype to modify the same property
console.log(typeof Array.prototype.sort);          // "function"
console.log(typeof String.prototype.substring);    // "function"
  • By prototyping native objects, you can not only get references to all default methods, but also define new methods.You can modify the prototype of a native object just as you modify the prototype of a custom object, so you can add methods at any time.The following code adds a method named startsWith() to the basic packaging type String
String.prototype.startsWith = function (text) {
  return this.indexOf(text) == 0;
};

var msg = "Hello world!";
console.log(msg.startsWith("Hello"));             // true
  • Although this may seem convenient, it is not recommended to modify the prototype of native objects in a product-oriented program.If a method is missing from one implementation, adding it to the prototype of the native object may result in naming conflicts when running code in another implementation that supports the method.This may also accidentally override the native method.

Problem with prototype objects

  • Nor is the prototype pattern flawless.

    • First, it omits passing initialization parameters to the constructor, resulting in all instances getting the same attribute value by default.Although this time it's inconvenient to some extent, it's not the biggest problem with prototypes.
    • The biggest problem with prototype patterns is the inherent locks they share.All the properties in the prototype are shared by many instances, and this sharing is ideal for functions.It's a good idea to talk about attributes that contain basic values, since you can hide the corresponding attributes in the prototype by adding a property with the same name to the instance.
    • However, the problem is highlighted when it comes to the inclusion of attributes that are worth referencing
    function Person() {}
    
    Person.prototype = {
      constructor: Person,                          // Redirect prototype constructor to Person
      name: "Nicholas",
      age: 29,
      job: "Software Engineer",
      friend: ["Shelby", "Court"],
      sayName: function () {
        console.log(this.name);
      }
    };
    
    var person1 = new Person();
    var person2 = new Person();
    
    // The modification here is actually Person.prototype.friends
    person1.friends.push("Van");
    
    // Not only was the friends property of person1 modified, but person2 did the same
    console.log(person1.friends);                               // "Shelby,Court,Van"
    console.log(person1.friends);                               // "Shelby,Court,Van"
    // Because the friends property of both instances points to Person.prototype.friends
    console.log(person1.friends === person2.friends);           // true
    
    • This is why few people use the prototype mode alone.

Combining constructor mode with prototype mode

  • The most common way to create custom types is to combine the constructor mode with the prototype mode.Constructor mode defines instance properties, while prototype mode defines methods and shared properties.This allows each instance to have its own copy of the instance properties, but at the same time shares a reference to the method, thus minimizing memory savings.
  • This blending mode also supports passing parameters to the constructor
  • This pattern of mixing constructors with prototypes is currently the most widely used and highly recognized way to create custom types in ECMAScript.It can be said to be a default mode for defining reference types.
function Person(name, age, job) {
  this.name = name;
  this.age = age;
  this.job = job;
  this.friends = ["Shelby", "Court"];
}

Person.prototype = {
  constructor: Person,
  sayName: function() {
    console.log(this.name);
  }
};

var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");

person1.friends.push("Van");
console.log(person1.friends);                               // "Shelby,Court,Van"
console.log(person1.friends);                               // "Shelby,Court"
console.log(person1.friends === person2.friends);           // false
console.log(person1.sayName === person2.sayName);           // true

Dynamic prototype mode

  • A dynamic prototype model encapsulates all information in a constructor, while maintaining the advantage of using both the constructor and the prototype by initializing the prototype in the constructor (only if necessary).In other words, you can decide whether you need to initialize a prototype by checking whether a method that should exist is valid
function Person(name, age, job) {
  // attribute
  this.name = name;
  this.age = age;
  this.job = job;
  // Method
  if (typeof this.sayName != "function") {
    Person.perototype.sayName = function() {
      console.log(this.name);
    };
  }
}

var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();                 // "Nicholas"
  • When using a dynamic prototype model, you cannot override the prototype using object literals.If you rewrite the prototype while you have already created it, you break the link between the existing instance and the new prototype.

Parasitic constructor mode

  • In cases where none of the above modes are applicable, you can use the parasitic constructor mode.Create a function that simply encapsulates the created object code and then returns the newly created object; however, on the surface, it looks like a typical constructor
function Person(name, age, job) {
  var o = new Object();
  o.name = name;
  o.age = age;
  o.job = job;
  o.sayName = function() {
    console.log(this.name);
  };
  return o
}

var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName();                 // "Nicholas"
  • In addition to using the new operator and calling the wrapper function used a constructor, this pattern is exactly the same as the factory pattern.When the constructor no longer returns a value, a new object instance is returned by default.With a return statement, you can override the value returned when the constructor is called.
  • This pattern can be used in special cases to create constructors for objects.Suppose we want to create a special array with an extra method.This pattern can be used because the Array constructor cannot be modified directly.
function SpecialArray() {

  // Create Array
  var values = new Array();

  // add value
  values.push.apply(values, arguments);

  // Add Method
  values.toPipedString = function() {
    return this.join("|");
  };

  // Return Array
  return values;
}

var colors = new SpecialArray("red", "blue", "green");
console.log(colors.toPipedString());                      // "red|blue|green"
  • One thing to note about the parasitic constructor pattern is that first, the returned object has no relationship with the constructor or with its prototype properties; that is, the object returned by the constructor is not different from the object created outside the constructor.For this reason, you cannot rely on the instanceof operator to determine the object type.Because of these issues, we recommend not using parasitic mode when other modes are available

Secure Constructor Mode

  • Douglas Crockford invented the concept of durable objects in JavaScript.A secure object is an object that does not have a public attribute and its method does not reference this.Secure objects are best used in secure environments where this and new are prohibited or when data is prevented from being altered by other applications, such as Mashup programs.Secure constructors follow a pattern similar to parasitic constructors, but differ in two ways:

    • One is that the instance method of creating the object does not reference this
    • Second, the constructor is not called with the new operator
function Person(name, age, job) {

  // Create an object to return
  var o = new Object();

  // Private variables and functions are defined here
  ...

  // Add Method
  o.sayName = function() {
    console.log(name);
  };

  // Return Object
  return o;
}

var friend = Person("Nicholas", 29, "Software Engineer");
friend.sayName();   // "Nicholas"
  • Among the objects created in this mode, there is no other way to access the value of name except by using the sayName() method.
  • Similar to the parasitic constructor pattern, objects created with the safe constructor pattern have nothing to do with the constructor, so the instanceof operator does not make sense for such objects.

inherit

  • Many OO languages support two forms of inheritance

    • Interface inheritance, method signature only
    • Implement inheritance, inherit the actual method
  • As mentioned earlier, interface inheritance is not possible in ECMAScript, only implementation inheritance is supported, and implementation inheritance is mainly achieved by prototype chains.

Prototype Chain

  • The primary method by which a prototype chain implements inheritance.The basic idea is to use prototypes to let one reference type inherit the properties and methods of another.A brief review of the relationships among constructors, prototypes, and instances:

    • Each constructor has a prototype object
    • Prototype objects all contain a pointer to a constructor
    • Instances contain an internal pointer to the prototype object.
  • If we make a prototype object equal to an instance of another type, it is clear that the prototype object at this point will contain a pointer to another prototype and, accordingly, a pointer to another constructor.If another prototype is another instance of another type, the above relationship still holds, so that the chain of instances and prototypes is formed by the progression of layers.This is the basic concept of the so-called prototype chain
  • There is a basic pattern for implementing a prototype chain
function SuperType() {
  this.property = true;
}

SuperType.prototype.getSuperValue = function() {
  return this.property;
};

function SubType() {
  this.subproperty = false;
}

// Inherited SuperType
SubType.prototype = new SuperType();

SubType.prototype.getSubValue = function () {
  return this.subproperty;
};

var instance = new SubType();
console.log(instance.getSuperValue());        //true
  • In the code above, instead of using the prototype SubType uses by default, we replaced it with a new prototype, an instance of SuperType.The new prototype not only has all the properties and methods that it has as an instance of SuperType, but also has a pointer inside that points to the prototype of SuperType.
  • Final:

    • instance points to the prototype of SubType
    • SubType's prototype points to SuperType's prototype
    • The getSuperValue() method is still in SuperType.prototype, but property is in SubType.prototype.This is because property is an instance property and getSuperValue() is a prototype method.Now that SubType.prototype is an instance of SuperType, property is, of course, in that instance.
    • Also, note that instance.constructor now points to SuperType because the constructor in the original SubType.prototype was overridden
  • By implementing the prototype chain, the prototype search mechanism is essentially expanded.When read mode accesses an instance property, it first searches the instance for that property.If this property is not found, the search continues for the prototype of the instance.With inheritance through the prototype chain, the search process can continue up along the prototype chain.Calling instance.getSuperValue() takes three search steps

    1. Search Instances
    2. Search SubType.prototype
    3. Find a method by searching SuperType.prototype

Don't forget the default prototype

  • In fact, the prototype chain in the preceding example is one less ring.All reference types inherit Object by default, and this inheritance is also achieved through the prototype chain.The default prototype for all functions is an instance of Object, because the default prototype contains an internal pointer to Object.prototype.This is why all custom types inherit default methods such as toString(), valueOf().
  • SubType inherits SuperType, while SuperType inherits Object.When you call the instance.toString() method, you are actually calling the method stored in Object.prototype

Determine the relationship between the prototype and the instance

  • First, use the instanceof operator, which returns true whenever you test a constructor that has appeared in the instance and prototype chains.
// Because of the prototype chain, we can be instance s of any type in Object, SuperType, SubType
console.log(instance instanceof Object);         // true
console.log(instance instanceof SuperType);      // true
console.log(instance instanceof SubType);        // true
  • Second, use the isPrototypeOf() method.Similarly, any prototype that appears in a prototype chain can be a prototype of an instance derived from that prototype chain.
console.log(Object.prototype.isPrototypeOf(instance));         // true
console.log(SuperType.prototype.isPrototypeOf(instance));      // true
console.log(SubType.prototype.isPrototypeOf(instance));        // true

Careful definition

  • Subtypes sometimes need to override a method in a supertype or add a method that does not exist in the supertype.Nevertheless, the code that adds a method to the prototype chain must be placed after the statement that replaces the prototype
function SuperType() {
  this.property = true;
}

SuperType.prototype.getSuperValue = function() {
  return this.property;
};

function SubType() {
  this.subproperty = false;
}

// Inherited SuperType, original default prototype replaced
SubType.protoype = new SuperType();

// Add a new method
SubType.prototype.getSubValue = function () {
  return this.subproperty;
};

// Override methods in supertypes
SubType.protoype.getSuperValue = function () {
  return false;
};

var instance = new SubType();
console.log(instance.getSuperValue());       // false
  • Another reminder is that when inheriting through a prototype chain, you cannot create prototype methods using object literals.Because this will override the prototype chain
function SuperType() {
  this.property = true;
}

SuperType.prototype. getSuperValue = function() {
  return this.property;
};

function SubType() {
  this.subproperty = false;
}

// Inherited SuperType
SubType.protype = new SuperType();

// Adding a new method using literal quantities invalidates the previous line of code
// The prototype chain is broken - there is no relationship between SubType and SuperType
SubType.prototype = {
  getSubValue: function () {
    return this.subproperty;
  },

  someOtherMethod: function () {
    return false;
  }
};

var instance = new SubType();
console.log(instance.getSuperValue()));    // error

Problem with prototype chains

  • The main problem comes from prototypes that contain values of reference types.Prototype properties containing reference type values are shared by all instances; therefore, values are defined in the constructor instead of prototype objects.When inheriting through a prototype, the prototype actually becomes an instance of another type.Thus, the original instance attributes will naturally become the prototype attributes now.
function SuperType() {
  this.colors = ["red", "blue", "green"];
}

function SubType() {
}

// After SubType inherits SuperType
// SubType.prototype becomes an instance of SuperType
// So SubType.prototype also has its own colors attribute, which is equivalent to creating a SubType.prototype.colors
SubType.protype = new SuperType();

var instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors);                // "red,blue,green,black"

// The result is that all SubType instances share this color property
var instance2 = new SubType();
console.log(instance2.colors);                // "red,blue,green,black"
  • The second problem with prototype chains is that when you create the strength of a subtype, you cannot pass parameters to the supertype constructor.To be precise, there is no way to pass parameters to a supertype constructor without affecting all object instances.
  • For this reason, prototype chains are rarely used separately in practice

Borrow Constructor

  • In solving the problem of including reference type values in a prototype, developers begin to use a technique called constructor stealing (sometimes referred to as fake objects or classical inheritance).
  • The idea is fairly simple, that is, to call a supertype constructor inside a subtype constructor.Functions are simply objects that execute code in a specific environment, so constructors can also be executed on newly created objects (in the future) by using the apply() call() method.In fact, in the environment of the newly created ubType instance (to be created in the future), calling the SuperType constructor executes all the object initialization code defined in the SuperType() function on the new SubType object.As a result, each instance of SubType will have its own copy of the colors property.
function SuperType() {
  this.colors = ["red", "blue", "green"];
}

function SubType() {
  // Inherited SuperType
  // Debugged a supertype constructor
  SuperType.call(this);
}

var instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors);                // "red,blue,green,black"

// SubType instances will not share this color property
var instance2 = new SubType();
console.log(instance2.colors);                // "red,blue,green"

Pass-through parameters

  • Borrowing constructors has a great advantage over prototype chains in that they allow you to pass parameters to supertype constructors in subtype constructors.
  • To ensure that the SuperType constructor does not override the properties of a subtype, you can add the properties that should be defined in the subtype after calling the supertype constructor.
function SuperType(name) {
  this.name = name;
}

function SubType() {
  // Inherited SuperType and passed parameters at the same time
  SuperType.call(this. "Nicholas");

  // Instance Properties
  this.age = 29;
}

var instance = new SubType();
console.log(instance.anme);             // "Nicholas"
console.log(instance.age);              // 29

Problems Borrowing Constructors

  • Just borrowing a constructor will not avoid the problem with the constructor pattern - methods are defined in the constructor, so function reuse is beyond question.
  • Defining a method in a supertype's prototype is also invisible to subtypes, resulting in all types using only the constructor pattern.
  • For this reason, borrowing constructors is rarely used alone

Combinatorial Inheritance

  • Combinatorial inheritance, sometimes called pseudoclassical inheritance, combines prototype chains with borrowed constructor techniques to make the most of both.The idea behind this is to use prototype chains to inherit prototype properties and methods, and borrow constructors to inherit instance properties.Function reuse is achieved by defining a method on the prototype, and each instance has its own properties
function SuperType(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function() {
  console.log(this.name);
}

function SubType(name, age) {
  // Inheritance Properties
  SuperType.call(this, name);

  // Subtypes own properties
  this.age = age
}

// Inheritance Method
SubType.prototype = new SuperType();
// SubType.prototype.constructor is SuperType if no constructor is specified
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
  console.log(this.age);
};

var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
console.log(instance1.colors);                 // "red,blue,green,black"
instance1.sayName();                           // "Nicholas"
instance1.sayAge();                            // 29

var instance2 = new SubType("Greg", 27);
console.log(instance2.colors);                 // "red,blue,green"
instance2.sayName();                           // "Greg"
instance2.sayAge();                            // 27
  • Combinatorial inheritance avoids the drawbacks of prototype chains and borrowed constructors, combines their advantages, and is the most common inheritance pattern in JavaScript.instanceof and isPrototypeOf() can also be used to identify objects created based on composite inheritance.

Prototype Inheritance

  • Douglas Crockford introduced an approach to inheritance in 2006, which does not use a strictly constructor.The idea was that prototypes could create new objects from existing objects without creating custom types.To achieve this, the following functions are given
function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}
  • Inside the object() function, a temporary constructor is created, the incoming object is prototyped, and a new instance of the temporary type is returned.Essentially, object() performs a shallow copy of the object passed in
var person = {
  name: "Nicholas",
  friends: ["Shelby", "Court", "Van"],
};

var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
console.log(anotherPerson.friends);             // "Shelby,Court,Van,Rob"

var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
console.log(yetAnotherPerson.friends);          // "Shelby,Court,Van,Rob,Barbie"

console.log(person.friends);                    // "Shelby,Court,Van,Rob,Barbie"
  • This prototype inheritance requires that you have one object as the basis for another.Pass it to the object() function and modify the resulting object to suit your specific needs.This means that person.friends is not only a person, but also shared by anotherPerson, yetAnotherPerson.This is essentially equivalent to creating two more copies (shallow copies) of the person object.
  • ECMAScript 5 normalizes prototype inheritance by adding the Object.create() method.This method accepts two parameters: an object to prototype a new object and (optionally) an object whose new object defines additional properties.The Object.create() method behaves the same as the Object() method when a parameter is passed in (as previously stated, but not in practice, with reference to the supplementary notes on the Object type in Chapter V)
var person = {
  name: "Nicholoas",
  friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
console.log(anotherPerson.friends);             // "Shelby,Court,Van,Rob"

var yetAnotherPerson = Object.object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
console.log(yetAnotherPerson.friends);          // "Shelby,Court,Van,Rob,Barbie"

console.log(person.friends);                    // "Shelby,Court,Van,Rob,Barbie"
  • The second parameter of the Object.create() method has the same format as the second parameter of the Object.defineProperties() method: each property is defined by its own descriptor.Any attribute formulated in this way overrides an attribute with the same name on the object.
var person = {
  name: "Nicholoas",
  friends: ["Shelby", "Court", "Van"]
};

var anotherPerson = Object.create(person, {
  name: {
    value: "Greg"
  }
});

console.log(anotherPerson.name);           // "Greg"
  • Browsers that support the Object.create() method: IE9+, Firefox4+, Opera12+, Chrome
  • Prototype inheritance is perfectly competent when there is no need to inspire creative creators but to keep one object similar to another.But don't forget that values containing reference types, such as the friends property above, are always shared as if they were in the prototype mode.

Parasitic Inheritance

  • parasitic inheritance is a thought closely related to prototype inheritance and was also promoted by Crockford.
  • Similar to the constructor and factory mode, the idea is to create a function that encapsulates the inheritance process only, enhances the object in some way internally, and finally returns the object as if it really did all the work.
function createAnother(original) {
  var clone = object(original);         // Create a new object by calling a function
  clone.sayHi = function() {            // Enhance this object in some way
    console.log("hi");
  };
  return clone;                         // Return this object
}

Parasitic Combinatorial Inheritance

  • Combinatorial inheritance is the most common inheritance pattern; however, the biggest problem is that the supertype constructor is called twice in any case:

    • One time when creating a prototype of a subtype
    • Another time inside the subtype constructor
  • That is, subtypes eventually contain all the instance properties of the supertype object, but we have to override them when we call the subtype constructor.
function SuperType(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function() {
  console.log(this.name);
};

function SubType(name, age) {
  SuperType.call(this, name);               // The second call to SuperType()

  this.age = age;
}

// Instantiate SuperType as a prototype of SubType
// Trigger immediately
SubType.prototype = new SuperType();        // First call to SuperType()
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function() {
  console.log(this.age);
};

// This triggers the second call
var instance = new SubType("Nicholas", 29);

console.log(instance.name);                // "Nicholas"
console.log(SubType.prototype.name);       // undefined
  • When the SuperType constructor is first called, SubType.prototype gets two attributes: name and colors; both are instance attributes of SuperType, but are now in the prototype of SubType.When the SubType constructor is called, the SuperType constructor is called again, this time creating the instance properties name and colors on the new object.These two attributes then block the two attributes with the same name in the prototype.(Fig. 6-6)

  • There are two sets of name and colors attributes, one on instance and one on the prototype of SubType.This is the result of two calls to the SuperType constructor
  • Parasitic combinatorial inheritance refers to inheriting attributes by borrowing constructors and methods by mixing prototype chains.The idea behind this is that instead of calling a supertype constructor to specify the prototype of a subtype, all we need is a copy of the supertype prototype.Essentially, you inherit the supertype's prototype using parasitic inheritance, then assign the result to the subtype's prototype
function inheritPrototype(subType, superType) {

  // Create a copy of the object-supertype prototype
  // The new operator is not used here, so no instance of SuperType is generated
  // SuperType constructor not called here
  var prototype = Object(superType.prototype)
  console.log(prototype == superType.prototype)

  // Enhanced Object - Compensates for the default constructor property lost by overriding the prototype
  // This also causes supert.prototype.constructor to point to subType
  prototype.constructor = subType;

  // Specify Object - Assign the newly created object (i.e. copy) to the prototype of the subtype.
  subType.prototype = prototype;
}

function SuperType(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function() {
  console.log(this.name);
};

function SubType(name, age) {
  SuperType.call(this, name);

  this.age = age;
}

inheritPrototype(SubType, SuperType);

SubType.prototype.sayAge = function() {
  console.log(this.age);
};
  • The inheritPrototype() function implements the simplest form of parasitic combinatorial inheritance.This function accepts two parameters: a subtype constructor and a supertype constructor.
  • This calls the SuperType constructor only once, and therefore creates unnecessary, redundant attributes on SubType.prototype.At the same time, the prototype chain remains unchanged; therefore, instanceof and isPrototypeOf() can also be used normally.
  • Parasitic combinatorial inheritance is generally accepted by developers as the best inheritance paradigm for reference types.

Posted by cshaw on Sat, 18 May 2019 12:37:07 -0700