Understanding archetypes:
- Reference types, all with object properties, can be freely extended attributes.
- Reference types have an implicit stereotype proto property whose value is a normal object.
- The property value of the implicit stereotype proto refers to the value of its constructor’s explicit prototype property.
- When you try to get a property of an object, if the object itself does not have the property, it will look in its implicit prototype (that is, its constructor’s explicit prototype).
About the object
Create an object
The factory pattern
function createPerson(name, age, job) {
let o = new Object(a); o.name = name; o.age = age; o.job = job; o.sayName =function() {
console.log(this.name);
};
return o;
}
let person1 = createPerson("Nicholas".29."Software Engineer");
let person2 = createPerson("Greg".27."Doctor");
Copy the code
Here, the function createPerson() takes three arguments from which to build an object containing information about the Person. This function can be called multiple times with different arguments, each time returning an object with three properties and one method. While this factory pattern solves the problem of creating multiple similar objects, it does not solve the problem of object identification (that is, what type the newly created object is).
Constructor pattern
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function() {
console.log(this.name);
};
}
let person1 = new Person("Nicholas".29."Software Engineer");
let person2 = new Person("Greg".27."Doctor");
person1.sayName(); // Nicholas
person2.sayName(); // Greg
Copy the code
In this example, the Person() constructor replaces the createPerson() factory function. In fact, the code inside Person() is basically the same as the code inside createPerson(), with the following differences.
- Objects are not explicitly created.
- Properties and methods are assigned directly to this.
- There is no return.
What does the new operator do?
- Creates a new object in memory.
- The [[proto]] property inside this new object is assigned to the constructor’s prototype property.
- The this inside the constructor is assigned to the new object (i.e. this refers to the new object).
- Executes the code inside the constructor (adding properties to the new object).
- If the constructor returns a non-empty object, the object is returned; Otherwise, the newly created object is returned.
The constructor problem
Constructors, while useful, are not without problems. The main problem with constructors is that they define methods that are created once on each instance. So for the previous example, person1 and Person2 both have methods named sayName(), but they are not the same Function instance. We know that functions in ECMAScript are objects, so each time a function is defined, an object is initialized. Logically, this constructor actually looks like this:
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = new Function("console.log(this.name)"); // Logical equivalence
}
Copy the code
Understanding the constructor this way makes it clear that each Person instance will have its own Function instance that displays the name attribute. Of course, creating a function this way introduces different scope chains and identifier resolution. But the mechanism for creating a new Function instance is the same. Log (person1.sayname == person2.sayname); // false There is no need to define two different instances of Function because they both do the same thing. Furthermore, the this object can defer the binding of a function to an object until runtime. To solve this problem, we can move the function definition outside of the constructor:
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}
function sayName() {
console.log(this.name);
}
let person1 = new Person("Nicholas".29."Software Engineer");
let person2 = new Person("Greg".27."Doctor");
person1.sayName(); // Nicholas
person2.sayName(); // Greg
Copy the code
Here, sayName() is defined outside the constructor. Inside the constructor, the sayName attribute is equal to the global sayName() function. Because this time the sayName attribute contains only a pointer to an external function, person1 and Person2 share the sayName() function defined at the global scope. This solves the problem of having functions with the same logic repeatedly defined, but it also messes up the global scope because that function can actually only be called on one object. If the object requires multiple methods, then multiple functions are defined at the global scope. This can cause code referenced by custom types to not aggregate well. This new problem can be solved with a prototype pattern.
The prototype pattern
Each function creates a Prototype property, which is an object containing properties and methods that should be shared by instances of a particular reference type. In effect, this object is the prototype of the object created by calling the constructor. The advantage of using a prototype object is that the properties and methods defined on it can be shared by the object instance. Values that were assigned directly to object instances in the constructor can be assigned directly to their prototypes, as follows:
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
let person1 = new Person();
person1.sayName(); // "Nicholas"
let person2 = new Person();
person2.sayName(); // "Nicholas"
console.log(person1.sayName == person2.sayName); // true
Copy the code
Here, all the properties and the sayName() method are added directly to the Person’s Prototype property, and there is nothing in the constructor body. But after this definition, the new object created by calling the constructor still has the corresponding properties and methods. Unlike the constructor pattern, the properties and methods defined using this prototype pattern are shared by all instances. So person1 and Person2 both access the same properties and the same sayName() function. To understand this process, you must understand the nature of archetypes in ECMAScript.
/** * Two instances created by the same constructor * share the same prototype: */
console.log(person1.__proto__ === person2.__proto__); // true
Copy the code
In essence, isPrototypeOf () in the incoming parameters of [[proto]] to call its returns true when the object, as shown in the following: the console. The log (Person) prototype) isPrototypeOf (person1)); // true console.log(Person.prototype.isPrototypeOf(person2)); // True // understand console.log(person1 instanceof Person); // True Where person1 and Person2 are checked by calling the isPrototypeOf() method on the prototype object. Because both examples have links to Person.prototype inside, the results return true.
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
let person1 = new Person();
let person2 = new Person();
person1.name = "Greg";
console.log(person1.name); // "Greg", from the instance
console.log(person2.name); // "Nicholas", from prototype
Copy the code
Whenever you add a property to the object instance, the property will shadow the property of the same name on the prototype object, i.e. it will not be modified, but access to it will be blocked. Setting this property to NULL on the instance does not restore its association with the stereotype. However, you can use the DELETE operator to completely remove this attribute on the instance, allowing the identifier resolution process to continue searching for the prototype object.
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
let person1 = new Person();
let person2 = new Person();
person1.name = "Greg";
console.log(person1.name); // "Greg", from the instance
console.log(person2.name); // "Nicholas", from prototype
delete person1.name;
console.log(person1.name); // "Nicholas", from prototype
Copy the code
The hasOwnProperty() method is used to determine whether a property is on an instance or on a prototype object. This method is inherited from Object and returns true if the property exists on the calling Object instance. Pay attention to the ECMAScript Object. GetOwnPropertyDescriptor () method only is effective for instance attributes. To get the Descriptor for the stereotype properties, you must call object.getownProperty-Descriptor () directly on the stereotype Object.
Prototype and the in operator
There are two ways to use the in operator: alone and in a for-in loop. When used alone, the in operator returns true if the specified property is accessible through the object, whether that property is on the instance or on the stereotype.
When you use the in operator in a for-in loop, all properties that can be accessed through the object and can be enumerated are returned, including instance properties and stereotype properties. Instance properties that obscure the non-enumerable ([[Enumerable]] property in the prototype is set to false) property are also returned in the for-in loop, because developer defined properties are Enumerable by default. To get all enumerable instance properties on an Object, use the object.keys () method. This method takes an object as an argument and returns an array of strings containing the names of all of the object’s enumerable properties. Such as:
function Person() {}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function() {
console.log(this.name);
};
let keys = Object.keys(Person.prototype);
console.log(keys); // "name,age,job,sayName"
let p1 = new Person();
p1.name = "Rob";
p1.age = 31;
let p1keys = Object.keys(p1);
console.log(p1keys); // "[name,age]"
Copy the code
If you want to list all instance attributes, whether can be enumerated, the Object can be used. GetOwnPropertyNames ()
Attribute enumeration order
The for – in circulation, the Object. The keys (), Object, getOwnPropertyNames (), the Object. The getOwnPropertySymbols () and the Object. The assign () a big difference in terms of property enumeration sequence. The order of enumeration for the for-in loop and object.keys () is indefinite, depending on the JavaScript engine, and may vary from browser to browser. Object. GetOwnPropertyNames (), Object. GetOwnPropertySymbols () and the Object, the assign () enumeration sequence is deterministic. Enumerate numeric keys in ascending order, followed by string and symbolic keys in insertion order. The keys defined in object literals are inserted in their comma-separated order.
let k1 = Symbol('k1'),
k2 = Symbol('k2');
let o = {
1: 1.first: 'first',
[k1]: 'sym2'.second: 'second'.0: 0
};
o[k2] = 'sym2';
o[3] = 3;
o.third = 'third';
o[2] = 2;
console.log(Object.getOwnPropertyNames(o));
// ["0", "1", "2", "3", "first", "second", "third"]
console.log(Object.getOwnPropertySymbols(o));
// [Symbol(k1), Symbol(k2)]
Copy the code
Other archetypal syntax
In the previous example, person.prototype is overridden every time a property or method is defined. To reduce code redundancy and visually encapsulate the prototype functionality, it has become common practice to rewrite the prototype directly through an object literal that contains all the properties and methods, as shown in the following example:
function Person() {}
Person.prototype = {
name: "Nicholas".age: 29.job: "Software Engineer".sayName() {
console.log(this.name); }};Copy the code
In this example, person. prototype is set to equal a new object created through an object literal. The end result is the same, except for one problem: Once this is rewritten, the constructor property of Person.prototype does not point to Person. When a function is created, its Prototype object is also created and its constructor property is automatically assigned. This completely overrides the default Prototype Object, so its constructor property also points to a completely different new Object (Object constructor) instead of the original constructor. While the instanceof operator can reliably return values, we can no longer rely on the constructor attribute to identify types, as shown in the following example:
let friend = new Person();
console.log(friend instanceof Object); // true
console.log(friend instanceof Person); // true
Copy the code
Here, instanceof still returns true for both Object and Person. But the constructor property is now equal to Object instead of Person. If constructor’s value is important, you can specifically set its value when overriding the prototype object as follows:
function Person() {
}
Person.prototype = {
constructor: Person,
name: "Nicholas".age: 29.job: "Software Engineer".sayName() {
console.log(this.name); }};Copy the code
Note, however, that restoring the Constructor property in this way creates a property that [[Enumerable]] is true. The native constructor property, by default, is not enumerable. Therefore, if you are using an EcMAScript-compliant JavaScript engine, you might use the object.defineProperty () method to define the constructor property instead:
function Person() {}
Person.prototype = {
name: "Nicholas".age: 29.job: "Software Engineer".sayName() {
console.log(this.name); }};// Restore the constructor attribute
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false.value: Person
});
Copy the code
The dynamics of prototypes
Because the search for values from the stereotype is dynamic, any changes made to the stereotype object will be reflected on the instance even if the instance existed before the stereotype was modified. Here’s an example:
let friend = new Person();
Person.prototype.sayHi = function() {
console.log("hi");
};
friend.sayHi(); // "No problem!"
Copy the code
The above code starts by creating a Person instance and saving it in friend. Then a statement adds a method called sayHi() to person.prototype. Although the friend instance was created before the method was added, it still has access to the method. The main reason for this is the loose connection between the instance and the stereotype. Although properties and methods can be added to a stereotype at any time and are immediately reflected on all object instances, this is not the same as overwriting the entire stereotype. The [[Prototype]] pointer to the instance is automatically assigned when the constructor is called and does not change even if the Prototype is changed to a different object. Rewriting the entire stereotype breaks the link between the original stereotype and the constructor, but the instance still refers to the original stereotype. Remember, the instance only has a pointer to the stereotype, not to the constructor. Here’s an example:
function Person() {}
let friend = new Person();
Person.prototype = {
constructor: Person,
name: "Nicholas".age: 29.job: "Software Engineer".sayName() {
console.log(this.name); }}; friend.sayName();/ / error
Copy the code
In this example, the new instance of Person is created before the prototype object is overridden. The call to friend.sayname () results in an error. This is because firend refers to the original stereotype, which does not have the sayName attribute
The problem with prototypes
The prototype pattern is not without its problems. First, it reduces the ability to pass initialization parameters to the constructor, causing all instances to default to the same property value. This is inconvenient, but not the biggest problem with prototypes. The main problem with prototypes stems from their shared nature. We know that all properties on the stereotype are shared between instances, which is appropriate for functions. Properties that contain original values are also fine; as shown in the previous example, properties on stereotypes can be simply obscured by adding attributes of the same name to the instance. The real problem comes from attributes that contain reference values. Here’s an example:
function Person() {}
Person.prototype = {
constructor: Person,
name: "Nicholas".age: 29.job: "Software Engineer".friends: ["Shelby"."Court"].sayName() {
console.log(this.name); }};let person1 = new Person();
let person2 = new Person();
person1.friends.push("Van");
console.log(person1.friends); // "Shelby,Court,Van"
console.log(person2.friends); // "Shelby,Court,Van"
console.log(person1.friends === person2.friends); // true
Copy the code
Here, person.prototype has a property called friends that contains an array of strings. Then two instances of Person are created. Person1.friends adds a string to the array using the push method. Since the friends property exists on person. prototype and not person1, the new string will also be reflected on person2.friends (which points to the same array). If this is a deliberate attempt to share arrays across multiple instances, that’s fine. But in general, different instances should have their own copies of properties. This is why prototyping patterns are not usually used alone in real development.
inheritance
Inheritance is one of the most discussed topics in object-oriented programming. Many object-oriented languages support two types of inheritance: interface inheritance and implementation inheritance. The former inherits only the method signature; the latter inherits the actual method. Interface inheritance is not possible in ECMAScript because functions are not signed. Implementation inheritance is the only method of inheritance that ECMAScript supports, and this is done primarily through prototype chains.
Prototype chain
Ecma-262 defines the prototype chain as the primary inheritance method in ECMAScript. The basic idea is to inherit the properties and methods of multiple reference types through stereotypes. Review the relationship between constructors, stereotypes, and instances: each constructor has a stereotype object, the stereotype has a property that refers back to the constructor, and the instance has an internal pointer to the stereotype. What if the prototype is an instance of another type? That means that the stereotype itself has an internal pointer to the other stereotype, which in turn has a pointer to the other constructor. This creates a chain of stereotypes between the instance and the stereotype. This is the basic idea of a prototype chain.
Implementing the prototype chain involves the following code pattern:
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property;
};
function SubType() {
this.subproperty = false;
}
/ / inherit the SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function () {
return this.subproperty;
};
let instance = new SubType();
console.log(instance.getSuperValue()); // true
Copy the code
Also note that because the constructor property of subtype. prototype is overwritten to point to SuperType, instance.constructor also points to SuperType.
About the way
Subclasses sometimes need to override methods of the parent class, or add methods that the parent class does not have. To do this, these methods must be added to the stereotype after the stereotype has been assigned. Here’s an example:
function SuperType() {
this.property = true;
}
SuperType.prototype.getSuperValue = function() {
return this.property;
};
function SubType() {
this.subproperty = false;
}
/ / inherit the SuperType
SubType.prototype = new SuperType();
/ / new method
SubType.prototype.getSubValue = function () {
return this.subproperty;
};
// Override an existing method
SubType.prototype.getSuperValue = function () {
return false;
};
let instance = new SubType();
console.log(instance.getSuperValue()); // false
Copy the code
In the code above, the bold sections refer to two methods. The first method getSubValue() is a new method for SubType, while the second method getSuperValue() is a method that already exists on the prototype chain but is obscured here. This method is later called when getSuperValue() is called on the SubType instance. An instance of SuperType still calls the original method. The important thing is that both methods are defined after assigning the stereotype to an instance of SuperType.
The prototype chain problem
The prototype chain, while a powerful tool for implementing inheritance, has its problems. The main problem arises when the prototype contains reference values. As mentioned earlier when talking about stereotypes, the reference values contained in a stereotype are shared across all instances, which is why attributes are usually defined in constructors and not on stereotypes. When you use a stereotype to implement inheritance, the stereotype actually becomes an instance of another type. This means that the original instance properties are now the prototype properties. The second problem with prototype chains is that subtypes cannot pass arguments to the constructor of their parent type when instantiated. In fact, we cannot pass arguments into the constructor of the parent class without affecting all instances of the object. This, combined with the aforementioned problem of including reference values in stereotypes, results in a chain of stereotypes rarely being used alone.
Stealing constructors
To address the inheritance problem caused by stereotypes containing reference values, a technique called “constructor stealing” has become popular in the development community (this technique is sometimes called “object masquerading” or “classical inheritance”). The basic idea is simple: call the superclass constructor in the subclass constructor. Since functions are, after all, simple objects that execute code in a specific context, you can use the Apply () and call() methods to execute the constructor against the newly created object. Let’s look at the following example
this.colors = ["red"."blue"."green"];
}
function SubType() {
/ / inherit the SuperType
SuperType.call(this);
}
let instance1 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"
let instance2 = new SubType();
console.log(instance2.colors); // "red,blue,green"
Copy the code
Passing parameters
An advantage of stealing constructors over using the prototype chain is that you can pass arguments to the superclass constructor from within the subclass constructor. Here’s an example:
function SuperType(name){
this.name = name;
}
function SubType() {
// Inherit SuperType and pass parameters
SuperType.call(this."Nicholas");
// Instance properties
this.age = 29;
}
let instance = new SubType();
console.log(instance.name); // "Nicholas";
console.log(instance.age); / / 29
Copy the code
In this example, the SuperType constructor takes a parameter name and assigns it to an attribute. Passing this argument to the SuperType constructor in the SubType constructor actually defines the name attribute on the instance of SubType. To ensure that the SuperType constructor does not overwrite attributes defined by SubType, you can add additional attributes to the subclass instance after the superclass constructor is called.
The problem of embezzling constructors
The main drawback to stealing constructors is also the problem with using custom types in the constructor pattern: methods must be defined in constructors, so functions cannot be reused. In addition, subclasses cannot access methods defined on the parent prototype, so all types can only use the constructor pattern. Because of these problems, pirate constructors cannot be used in isolation.
Combination of inheritance
Composite inheritance (sometimes called pseudo-classical inheritance) combines the best of both stereotypes and misappropriated constructors. The basic idea is to use the stereotype chain to inherit the properties and methods on the stereotype, and to inherit the instance properties by stealing the constructor. This allows methods to be defined on stereotypes for reuse, while allowing each instance to have its own properties. Here’s an example:
function SuperType(name){
this.name = name;
this.colors = ["red"."blue"."green"];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
function SubType(name, age){
// Inherit attributes
SuperType.call(this, name);
this.age = age;
}
// Inherit the method
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function() {
console.log(this.age);
};
let instance1 = new SubType("Nicholas".29);
instance1.colors.push("black");
console.log(instance1.colors); // "red,blue,green,black"
instance1.sayName(); // "Nicholas";
instance1.sayAge(); / / 29
let instance2 = new SubType("Greg".27);
console.log(instance2.colors); // "red,blue,green"
instance2.sayName(); // "Greg";
instance2.sayAge(); / / 27
Copy the code
Composite inheritance is the most commonly used inheritance pattern in JavaScript, making up for the lack of prototype chains and misappropriated constructors. And composition inheritance preserves the ability of the instanceof operator and isPrototypeOf() method to recognize synthesized objects.
Original type inheritance
Douglas Crockford defines a function in Prototype inheritance in JavaScript:
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
Copy the code
The object() function creates a temporary constructor, assigns the passed object to the prototype of the constructor, and then returns an instance of the temporary type. Essentially, object() performs a shallow copy of the object passed in. Here’s an example:
let person = {
name: "Nicholas".friends: ["Shelby"."Court"."Van"]};let anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
let yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
console.log(person.friends); // "Shelby,Court,Van,Rob,Barbie"
Copy the code
Prototype inheritance works when you have an object and want to create a new object on top of it. You need to pass this object to object() first, and then modify the returned object as appropriate. In this example, Person defines information to the image that should also be shared by another object, and passing it to object() returns a new object. The stereotype of this new object is Person, which means that it has both the original value property and the reference value property on its stereotype. This also means that person.friends is not only a property of Person, but is also shared with anotherPerson and yetAnotherPerson. Here we actually clone two Persons. ECMAScript 5 normalizes the concept of prototype inheritance by adding the object.create () method. This method takes two arguments: the object that is the prototype of the new object, and (optional) the object that defines additional properties for the new object. With only one argument, object.create () has the same effect as the Object () method here:
let person = {
name: "Nicholas".friends: ["Shelby"."Court"."Van"]};let anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
let yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
console.log(person.friends); // "Shelby,Court,Van,Rob,Barbie"
Copy the code
The second argument to object.create () is the same as the second argument to object.defineProperties () : each new property is described by its own descriptor. Properties added in this way obscure properties of the same name on the prototype object. Such as:
let person = {
name: "Nicholas".friends: ["Shelby"."Court"."Van"]};let anotherPerson = Object.create(person, {
name: {
value: "Greg"}});console.log(anotherPerson.name); // "Greg"
Copy the code
Prototype inheritance is ideal for situations where you don’t need to create a separate constructor, but you still need to share information between objects. Keep in mind, however, that reference values contained in properties are always shared between related objects, just as with the stereotype pattern.
Parasitic inheritance
A similar inheritance method to the original type is parasitic inheritance, which is also a model initiated by Crockford. The idea behind parasitic inheritance is similar to the parasitic constructor and factory pattern: create a function that implements inheritance, enhance an object in some way, and then return that object. The basic parasitic inheritance pattern is as follows:
function createAnother(original){
let clone = object(original); // Create a new object by calling a function
clone.sayHi = function() { // Enhance the object in some way
console.log("hi");
};
return clone; // Return this object
}
Copy the code
Parasitic combination inheritance
Parasitic composite inheritance inherits properties by stealing constructors, but uses the hybrid prototype chain inheritance method. The basic idea is to get a copy of the parent prototype instead of assigning a value to the parent prototype by calling the parent constructor. The bottom line is to use parasitic inheritance to inherit from the parent stereotype and then assign the new object returned to the subclass stereotype. The basic pattern of parasitic composite inheritance is as follows:
function inheritPrototype(subType, superType) {
let prototype = object(superType.prototype); // Create an object
prototype.constructor = subType; // Enhance the object
subType.prototype = prototype; // Assign objects
}
Copy the code
The inheritPrototype() function implements the core logic of parasitic composition inheritance. This function takes two arguments: the subclass constructor and the superclass constructor. Inside this function, the first step is to create a copy of the superclass prototype. We then set the constructor property to the returned Prototype object, resolving the problem that default constructor was lost due to overwriting the prototype. Finally, the newly created object is assigned to the prototype of the subtype. As shown in the following example, calling inheritPrototype() implements the subtype stereotype assignment in the previous example:
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);
};
Copy the code
The SuperType constructor is called only once, avoiding unnecessary and unneeded properties on subType. prototype, so this example is arguably more efficient. Also, the prototype chain remains the same, so the instanceof operator and isPrototypeOf() method work properly. Parasitic composite inheritance can be the best model for reference type inheritance.
Text content borrowed from @ Nick Chen and advanced programming