This article has been machine-translated from Chinese. The translation may contain inaccuracies or awkward phrasing. If in doubt, please refer to the original Chinese version.
Understanding objects and their creation process, ES6 syntactic sugar, the prototype pattern, and constructors.
Understanding prototype chains, constructor stealing, combination inheritance, and best practices.
Understanding Objects
ECMA-262 defines an object as an unordered collection of properties, each with a name that maps to a value. You can think of it as a hash table where values can be data or functions.
- Example 1
// Creating with new Object()
let person = new Object();
person.name = 'cosine';
person.age = 29;
person.job = 'Software Engineer';
person.sayName = function () {
console.log(this.name);
};
// Creating with object literal
let person = {
name: 'cosine',
age: 29,
job: 'Software Engineer',
sayName() {
console.log(this.name);
},
};
The two objects above are equivalent, sharing the same properties and methods.
Consider this: What happens during the new process? This will be discussed later as well.
Property Types
ECMA-262 uses internal attributes to describe the characteristics of properties. These attributes are defined by the specification for JavaScript engine implementations. Therefore, developers cannot directly access these attributes in JavaScript. To identify an attribute as internal, the specification wraps the attribute name in double brackets, such as [[Enumerable]].
Properties come in two types: data properties and accessor properties.
Data Properties
Data properties contain a location for a data value. Values are read from and written to this location. Data properties have 4 attributes that describe their behavior:
[[Configurable]]Configurable- Indicates whether the property can be deleted and redefined via
delete - Whether its attributes can be modified
- Whether it can be changed to an accessor property
- By default, this attribute is
truefor all properties defined directly on an object
- Indicates whether the property can be deleted and redefined via
[[Enumerable]]Enumerable- Indicates whether the property can be returned through a
for-inloop - Default:
true
- Indicates whether the property can be returned through a
[[Writable]]Writable- Indicates whether the property’s value can be modified
- Default:
true
[[Value]]Value- Contains the property’s actual value
- This is the location for reading and writing mentioned earlier
- Default:
undefined
After explicitly adding properties to an object as in the example above, [[Configurable]], [[Enumerable]], and [[Writable]] are all set to true, while the [[Value]] attribute is set to the specified value.
Object.defineProperty() Method
To modify the default attributes of a property, you must use the Object.defineProperty() method. This method accepts 3 arguments:
objThe object to which the property will be addedpropThe property name or Symbol to define or modifydescriptorThe property descriptor to define or modify
Here’s an example of how to use it:
// Setting properties with Object.defineProperty()
let person = {};
Object.defineProperty(person, 'name', {
writable: false, // Look here! Not modifiable
value: 'cosine',
});
console.log(person.name); // cosine
person.name = 'NaHCOx'; // Attempt to modify
console.log(person.name); // Modification ignored, prints: cosine
A property named name was created with a read-only value, so the value cannot be modified.
- In non-strict mode, attempting to reassign this property will be silently ignored.
- In strict mode, attempting to modify a read-only property will throw an error.
Similar rules apply to creating non-configurable properties.
Setting configurable to false means the property cannot be deleted from the object.
Note: Once a property is defined as non-configurable, it cannot be changed back to configurable!!
Calling
Object.defineProperty()again and modifying any attribute other thanwritablewill result in an error.
When calling Object.defineProperty(), if configurable, enumerable, and writable are not specified, they all default to false.
Accessor Properties
Accessor properties do not contain a data value. Instead, they contain a getter (get) function and a setter (set) function, though neither is required.
- When reading an accessor property, the
getterfunction is called and returns a valid value - When writing to an accessor property, the
setterfunction is called with the new value, and this function must decide what modifications to make to the data
Accessor properties have 4 attributes that describe their behavior:
[[Configurable]]: Indicates whether the property- Can be deleted and redefined via delete
- Whether its attributes can be modified
- Whether it can be changed to a data property
- Default:
true
[[Enumerable]]: Indicates whether the property can be returned through a for-in loop. Default:true[[Get]]: Getter function, called when reading the property. Default:undefined[[Set]]: Setter function, called when writing to the property. Default:undefined
Note that the defaults listed above apply when defining properties directly on an object. If using Object.defineProperty(), any undefined attributes default to undefined.
Here is an example:
// Accessor property definition
// Define an object with a pseudo-private member year_ and a public member edition
let book = {
year_: 2017,
edition: 1,
};
Object.defineProperty(book, 'year', {
get() {
return this.year_;
},
set(newVal) {
if (newVal > 2017) {
this.year_ = newVal;
this.edition += newVal - 2017;
}
},
});
book.year = 1999;
console.log(book.year); // 2017
console.log(book.edition); // 1
book.year = 2018;
console.log(book.year); // 2018
console.log(book.edition); // 2
The object book has two default properties: year_ and edition. The underscore in year_ is commonly used to indicate that the property is not intended to be accessed outside of the object’s methods. Another property, year, is defined as an accessor property where:
- The getter simply returns the value of year_
- The setter performs a calculation to determine the correct edition
Therefore, setting the year property to 2018 causes year_ to become 2018 and edition to become 2. Attempting to set it to 1999 has no effect. This is a typical use case for accessor properties, where setting one property value causes other changes to occur.
Getter and setter functions don’t both need to be defined.
- Defining only a
getterfunction means the property is read-only; attempts to modify it will be ignored. In strict mode, attempting to write to a property with only a getter will throw an error. - Similarly, a property with only a
settercannot be read. In non-strict mode, reading will returnundefined; in strict mode, it will throw an error.
Other Property Definition Functions
You can also define multiple properties via Object.defineProperties(), use Object.getOwnPropertyDescriptor() to retrieve the property descriptor for a specified property, and use Object.getOwnPropertyDescriptors() to retrieve the property descriptor for each own property and return them in a new object.
Merging Objects
Copying all properties from a source object to a target object is also called “mixin,” because the target object is enhanced by mixing in properties from the source.
Object.assign() copies the values of all enumerable/own properties from one or more source objects to a target object and returns the target object.
const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };
const returnedTarget = Object.assign(target, source);
console.log(target);
// expected output: Object { a: 1, b: 4, c: 5 }
console.log(returnedTarget);
// expected output: Object { a: 1, b: 4, c: 5 }
Object.assign() actually performs a shallow copy, only copying references to objects.
- If multiple source objects have the same property, the last copied value is used.
- Values obtained from accessor properties on source objects, such as getters, are assigned as static values to the target object. In other words: getter and setter functions cannot be transferred between two objects.
- If an error occurs during assignment, the operation aborts and throws an error. There is no concept of “rolling back” previous assignments, so it is a best-effort method that may only partially complete the copy.
Object Identity and Equality Determination
Before ES6, some cases could not be properly determined using the === operator, such as:
// These cases behave differently across JavaScript engines but are still considered equal
console.log(+0 === -0); // true
console.log(+0 === 0); // true
console.log(-0 === 0); // true
// To determine NaN equality, you must use the annoying isNaN()
console.log(NaN === NaN); // false
console.log(isNaN(NaN)); // true
The ES6 specification improved these situations by adding the Object.is() method to determine whether two values are the same value:
// Correct +0, -0, 0 equality/inequality determination
console.log(Object.is(+0, -0)); // false
console.log(Object.is(+0, 0)); // true
console.log(Object.is(-0, 0)); // false
// Correct NaN equality determination
console.log(Object.is(NaN, NaN)); // true
To check more than two values, you can recursively use the transitivity of equality:
function recursivelyCheckEqual(x, ...rest) {
return Object.is(x, rest[0]) && (rest.length < 2 || recursivelyCheckEqual(...rest));
}
ES6 Syntactic Sugar
ECMAScript 6 introduced many extremely useful syntactic sugar features for defining and manipulating objects. None of these features changed existing engine behavior, but they greatly improved the convenience of working with objects.
Property Shorthand
When adding variables to an object, you often find that the property name and variable name are the same. In this case, you can just use the variable name without the colon. If no variable with the same name is found, a ReferenceError is thrown.
For example:
let name = 'cosine';
let person = { name: name };
console.log(person); // { name: 'cosine' }
// Using syntactic sugar, equivalent to the above
let person = { name };
console.log(person); // { name: 'cosine' }
Code compressors preserve property names across different scopes to prevent missing references.
Computed Properties
You can dynamically name properties directly in object literals:
const nameKey = 'name';
const ageKey = 'age';
const jobKey = 'job';
let uniqueToken = 0;
function getUniqueKey(key) {
return `${key}_${uniqueToken++}`;
}
let person = {
[nameKey]: 'cosine',
[ageKey]: 21,
[jobKey]: 'Software engineer',
// Can also be an expression!
[getUniqueKey(jobKey + ageKey)]: 'test',
};
console.log(person);
// { name: 'cosine', age: 21, job: 'Software engineer', jobage_0: 'test' }
Shorthand Method Names
Let’s look directly:
let person = {
let person = {
// sayName: function(name) { // old way
// console.log(`My name is ${name}`);
// }
sayName(name) { // new way
console.log(`My name is ${name}`);
}
};
person.sayName('Matt'); // My name is Matt
Shorthand method names also work for getter and setter functions, and are compatible with computed property keys. This also lays the foundation for classes discussed later.
Object Destructuring
// Object destructuring
let person = {
name: 'cosine',
age: 21,
};
let { name: personName, age: personAge } = person;
console.log(personName, personAge); // cosine 21
// Let variables use property names directly, define default values; undefined properties without defaults are undefined
let { name, age, job = 'test', score } = person;
console.log(name, age, job, score); // cosine 21 test undefined
Destructuring internally uses the ToObject() function (not directly accessible in the runtime environment) to convert the source data structure into an object.
This means that in the context of object destructuring, primitive values are treated as objects. In other words: null and undefined cannot be destructured, or an error will be thrown.
let { length } = 'foobar';
console.log(length); // 6
let { constructor: c } = 4;
console.log(c === Number); // true
let { _ } = null; // TypeError
let { _ } = undefined; // TypeError
To destructure and assign to previously declared variables, the assignment expression must be wrapped in parentheses:
let personName, personAge;
let person = {
name: 'cosine',
age: 21,
};
({ name: personName, age: personAge } = person);
console.log(personName, personAge); // cosine 21
1. Nested Destructuring
Destructuring has no restrictions on referencing nested properties or assignment targets. You can use destructuring to copy object properties (shallow copy):
let person = {
name: 'cosine',
age: 21,
job: {
title: 'Software engineer',
},
};
// Declare title variable and assign the value of person.job.title to it
let {
job: { title },
} = person;
console.log(title); // Software engineer
Nested destructuring cannot be used when the outer property is undefined. This applies to both source and target objects.
2. Partial Destructuring
Destructuring assignment involving multiple properties is an output-independent sequential operation. If a destructuring expression involves multiple assignments where earlier ones succeed but later ones fail (destructuring undefined or null), the entire destructuring assignment will only partially complete.
3. Parameter Context Matching
Destructuring assignment can also be performed in function parameter lists. Destructuring assignment on parameters does not affect the arguments object, but allows declaring local variables for use within the function body:
let person = {
name: 'cosine',
age: 21,
};
function printPerson(foo, { name, age }, bar) {
console.log(arguments);
console.log(name, age);
}
function printPerson2(foo, { name: personName, age: personAge }, bar) {
console.log(arguments);
console.log(personName, personAge);
}
printPerson('1st', person, '2nd');
// ['1st', { name: 'cosine', age: 21 }, '2nd']
// 'cosine' 21
printPerson2('1st', person, '2nd');
// ['1st', { name: 'cosine', age: 21 }, '2nd']
// 'cosine' 21
Creating Objects
Starting with ES6, classes and inheritance are officially supported, though this support is essentially syntactic sugar wrapping ES5.1’s constructor + prototype inheritance pattern.
Factory Pattern
Some design patterns were discussed in the design patterns blog post (Frontend Design Pattern Notes). The factory pattern is a widely used design pattern that provides an optimal way to create objects. In the factory pattern, the creation logic is not exposed to the client, and a common interface is used to point to newly created objects.
function createPerson(name, age, job) {
let o = new Object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function () {
console.log(this.name);
};
return o;
}
let person1 = createPerson('cosine', 21, 'Software Engineer');
let person2 = createPerson('Greg', 27, 'Doctor');
console.log(person1); // { name: 'cosine', age: 21, job: 'Software Engineer', sayName: [Function (anonymous)] }
person1.sayName(); // cosine
console.log(person2); // { name: 'Greg', age: 27, job: 'Doctor', sayName: [Function (anonymous)] }
person2.sayName(); // Greg
This pattern solves the problem of creating multiple similar objects, but does not solve the object identification problem (i.e., what type is the newly created object).
Constructor Pattern
Custom constructors define properties and methods for your own object types in function form.
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('cosine', 21, 'Software Engineer');
person1.sayName(); // cosine
- No explicit object creation
- Properties and methods are assigned directly to this
- No return statement
- To create a Person instance, use the new operator
What Happens During the new Process?
Key point: using new to call a constructor performs the following operations:
- Create a new object in memory
- Assign the new object’s internal
[[Prototype]]to the constructor’s prototype property - Point
thisinside the constructor to this new object - Execute the code inside the constructor (adding properties to the object)
- If the constructor returns a non-empty object, return that object. Otherwise, return the newly created object!
At the end of the previous example, person1 has a constructor property pointing to Person:
console.log(person1.constructor); // [Function: Person]
console.log(person1.constructor === Person); // true
console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true
Defining custom constructors ensures instances are identified as a specific type, which is a significant advantage over the factory pattern. person1 is also considered an instance of Object because all custom objects inherit from Object (discussed later).
Note the following:
- Constructors are also functions: Any function called with the new operator is a constructor; without new, it’s a regular function.
- The main problem with constructors: Methods defined within them are created anew on every instance. So functions with the same name on different instances are not equal, and since they do the same thing, there’s no need to define two different Function instances.
The this object allows deferring function-to-object binding until runtime, so function definitions can be moved outside the constructor. While this solves the problem of duplicate function definitions for the same logic, it pollutes the global scope because those functions can really only be called on one object. If the object needs multiple methods, multiple functions must be defined in the global scope, causing the custom type’s referenced code to not be well organized. This new problem can be solved with the prototype pattern.
Prototype Pattern
- Every function creates a
prototypeproperty pointing to the prototype object - Properties and methods defined on the prototype object can be shared by all object instances
- Values originally assigned to object instances in the constructor can be assigned directly to their prototype
1. Understanding Prototypes
- Whenever a function is created, a
prototypeproperty is created for it according to specific rules, pointing to the prototype object - All prototype objects get a property named
constructorthat points back to the associated constructor- For example,
Person.prototype.constructorpoints toPerson
- For example,
- Additional properties and methods can be added to the prototype object through the constructor
- Prototype objects by default only have the
constructorproperty; all other methods are inherited fromObject - Each time the constructor is called to create a new instance, the instance’s internal
[[Prototype]]pointer is assigned to the constructor’s prototype object - There is no standard way to access the
[[Prototype]]attribute in scripts, but Firefox, Safari, and Chrome expose a__proto__property on each object, through which you can access the object’s prototype
The normal prototype chain terminates at Object’s prototype, and Object’s prototype’s prototype is null:
console.log(Person.prototype.__proto__ === Object.prototype); // true
console.log(Person.prototype.__proto__.constructor === Object); // true
console.log(Person.prototype.__proto__.__proto__ === null); // true
Constructors, prototype objects, and instances are 3 completely different objects:
console.log(person1 !== Person); // true
console.log(person1 !== Person.prototype); // true
console.log(Person.prototype !== Person); // true
Instances link to the prototype object through __proto__, and constructors link to the prototype object through the prototype property:
console.log(person1.__proto__ === Person.prototype); // true
conosle.log(person1.__proto__.constructor === Person); // true
Instances created by the same constructor share the same prototype object, and instanceof checks whether the specified constructor’s prototype appears in the instance’s prototype chain:
console.log(person1.__proto__ === person2.__proto__); // true
console.log(person1 instanceof Person); // true
console.log(person1 instanceof Object); // true
- The
isPrototypeOf()method tests whether an object exists in another object’s prototype chain.- Unlike the
instanceofoperator. In the expressionobject instanceof AFunction, object’s prototype chain is checked againstAFunction.prototype, not againstAFunctionitself.
- Unlike the
- The
getPrototypeOf()method returns the value of the argument’s internal[[Prototype]]attribute.- Using it makes it easy to retrieve an object’s prototype, which is especially important when implementing inheritance through prototypes.
- The
setPrototypeOf()method can write a new value to an instance’s private[[Prototype]]attribute.- Using it can rewrite an object’s prototype inheritance relationship.
console.log(Person.prototype.isPrototypeOf(person1)); // true
console.log(Person.prototype.isPrototypeOf(person2)); // true
console.log(Object.getPrototypeOf(person1) == Person.prototype); // true
console.log(Object.getPrototypeOf(person1).name); // "cosine"
Object.setPrototypeOf()can seriously impact code performance. The Mozilla documentation makes it very clear: “In all browsers and JavaScript engines, the impact of modifying inheritance relationships is subtle and far-reaching. This impact is not simply limited to theObject.setPrototypeOf()statement itself, but affects all code that accesses objects whose[[Prototype]]has been modified.”
To avoid the potential performance degradation from Object.setPrototypeOf(), you can use Object.create() to create a new object while specifying its prototype:
let biped = {
numLegs: 1,
};
let person = Object.create(biped);
person.name = 'cosine';
console.log(person.name); // cosine
console.log(person.numLegs); // 1
console.log(Object.getPrototypeOf(person) === biped); // true
2. Prototype Hierarchy
- When accessing a property through an object, a search is performed using the property name.
- If the property is found on the instance, the corresponding value is returned.
- If it’s not found on the instance, the prototype object is searched. After finding the property on the prototype, the corresponding value is returned.
- If not found on the prototype, the prototype’s prototype is searched… and so on until found.
- This is the mechanism by which prototypes share properties and methods across multiple object instances.
Note the following:
- Although you can read values from the prototype through an instance, you cannot overwrite values on the prototype through an instance.
- Adding a property to an instance with the same name as one on the prototype will create the property on the instance, and this property will shadow the one on the prototype.
- Using the
deleteoperator can completely remove the property from the instance, allowing the identifier resolution process to continue searching the prototype object.
hasOwnProperty()
The hasOwnProperty() method is used to determine whether a property exists on the instance or on the prototype object. This method returns true when the property exists on the calling object instance:
function Person() {}
Person.prototype.name = 'cosine';
Person.prototype.age = 21;
Person.prototype.job = 'Software Engineer';
Person.prototype.sayName = function () {
console.log(this.name);
};
let person1 = new Person();
let person2 = new Person();
console.log(person1.hasOwnProperty('name')); // false
person1.name = 'Khat'; // Added instance property 'name', shadows the prototype's 'name'
console.log(person1.name); // "Khat", from the instance
console.log(person1.hasOwnProperty('name')); // true
console.log(person2.name); // "cosine", from the prototype
console.log(person2.hasOwnProperty('name')); // false
delete person1.name;
console.log(person1.name); // "cosine", from the prototype
console.log(person1.hasOwnProperty('name')); // false
3. Prototypes and the in Operator
The in operator can be used in two ways:
- Standalone
inoperator - In a
for-inloop
When used standalone, it returns true when accessing a specified property through an object, regardless of whether the property is on the instance or the prototype:
console.log(person1.hasOwnProperty('name')); // false
console.log('name' in person1); // true
person1.name = 'cosine';
console.log(person1.name); // "Khat", from the instance
console.log(person1.hasOwnProperty('name')); // true
console.log('name' in person1); // true
console.log(person2.name); // "cosine", from the prototype
console.log(person2.hasOwnProperty('name')); // false
console.log('name' in person2); // true
delete person1.name;
console.log(person1.name); // "cosine", from the prototype
console.log(person1.hasOwnProperty('name')); // false
console.log('name' in person1); // true
To determine whether a property exists on the prototype, you can combine hasOwnProperty() and the in operator:
function hasPrototypeProperty(object, name) {
return !object.hasOwnProperty(name) && name in object;
}
When using the in operator in a for-in loop, all properties that can be accessed through the object and are enumerable will be returned, including enumerable instance and prototype properties (excluding shadowed prototype properties and non-enumerable properties). Additionally, the Object.keys() method can retrieve all enumerable instance properties of an object, returning a string array of all enumerable property names.
To get all instance properties (including non-enumerable ones), use Object.getOwnPropertyNames().
4. Property Enumeration Order
- Enumeration order is not guaranteed
for-inloopObject.keys()- Depends on the JavaScript engine and may vary by browser.
- Enumeration order is deterministic
Object.getOwnPropertyNames()Object.getOwnPropertySymbols()Object.assign()- Numeric keys are enumerated in ascending order first, then string and symbol keys in insertion order.
Object Iteration
ECMAScript 2017 added two static methods for converting object contents into a serialized (iterable) format. These two static methods, Object.values() and Object.entries(), accept an object and return an array of its contents.
Object.values()returns an array of object valuesObject.entries()returns an array of key-value pairs- Non-string properties are converted to string output, and symbol properties are ignored. Both methods perform a shallow copy of the object.
1. Other Prototype Syntax
To reduce code redundancy and visually encapsulate prototype functionality, it became common practice to rewrite the prototype using an object literal containing all properties and methods:
function Person() {}
Person.prototype = {
name: 'cosine',
age: 21,
job: 'Software Engineer',
sayName() {
console.log(this.name);
},
};
But there’s a problem: after this rewrite, Person.prototype’s constructor property no longer points to Person. When creating a function, its prototype object is also created, and the constructor property on the prototype is automatically assigned. So we need to explicitly set the constructor value:
Person.prototype = {
constructor: Person, // assignment
name: 'cosine',
age: 21,
job: 'Software Engineer',
sayName() {
console.log(this.name);
},
};
Restoring the constructor property this way creates a property with [[Enumerable]] set to true. The native constructor property is non-enumerable by default. Therefore, if you’re using an ECMAScript-compliant JavaScript engine, you may want to use Object.defineProperty() to define the constructor property instead.
2. Dynamic Nature of Prototypes
Because the process of searching for values on prototypes is dynamic, even if an instance exists before the prototype is modified, any modifications to the prototype object will be reflected on the instance:
function Person() {}
let friend = new Person();
Person.prototype = {
constructor: Person,
name: 'cosine',
age: 21,
job: 'Software Engineer',
sayName() {
console.log(this.name);
},
};
friend.sayName(); // Error
3. Native Object Prototypes
- The prototype pattern is used to implement all native reference types.
- All native reference type constructors (including
Object,Array,String, etc.) define instance methods on their prototypes.- Array instance methods like
sort()are defined onArray.prototype - String wrapper object methods like
substring()are also defined onString.prototype
- Array instance methods like
- Through native object prototypes, you can access references to all default methods, and you can also define new methods for instances of native types (though this is not recommended).
4. Problems with Prototypes
- Weakens the ability to pass initialization parameters to constructors, causing all instances to have the same default property values
- The main problem stems from the shared nature: generally, different instances should have their own copies of properties, but properties added via the prototype pattern are reflected across all instances
Implementing Inheritance
Prototype Chain
- ECMA-262 defines the prototype chain as ECMAScript’s primary inheritance mechanism
- Basic idea: Through prototypes, inherit properties and methods of multiple reference types.
Recall: every constructor has a prototype object, linked via prototype to the prototype object. The prototype has a constructor property pointing back to the constructor, and instances have an internal pointer __proto__ pointing to the prototype.
What if the prototype is an instance of another type? That means this prototype itself has an internal pointer __proto__ pointing to another prototype object. Accordingly, that other prototype also has a constructor pointer pointing to another constructor. This creates a prototype chain between instances and prototypes.
The prototype chain extends the prototype search mechanism described earlier. We know that when reading a property on an instance, the instance is searched first. If not found, the instance’s prototype is searched. After implementing inheritance through the prototype chain, the search can go up, searching the prototype’s prototype, all the way to the end of the prototype chain.
1. Default Prototype
By default, all reference types inherit from Object, which is also achieved through the prototype chain. Any function’s default prototype is an instance of Object, which means this instance has an internal pointer to Object.prototype. This is why custom types can inherit all default methods including toString() and valueOf().
2. Prototype and Inheritance Relationship
The relationship between prototypes and instances can be determined in two ways:
- Using the
instanceofoperator: if a constructor ever appears in an instance’s prototype chain,instanceofreturnstrue
console.log(instance instanceof Object); // true
console.log(instance instanceof SuperType); // true
console.log(instance instanceof SubType); // true
- Using the
isPrototypeOf()method: as long as the instance’s prototype chain contains this prototype, the method returns true
console.log(Object.prototype.isPrototypeOf(instance)); // true
console.log(SuperType.prototype.isPrototypeOf(instance)); // true
console.log(SubType.prototype.isPrototypeOf(instance)); // true
3. Problems with the Prototype Chain
- As mentioned when discussing prototype problems, reference values contained in prototypes are shared among all instances. This is why properties are typically defined in the constructor rather than on the prototype.
- When using prototypes for inheritance, the prototype actually becomes an instance of another type. This means properties that were originally instance properties…
function SuperType() {
this.colors = ['red', 'blue', 'green'];
}
function SubType() {}
// Inherit SuperType
SubType.prototype = new SuperType();
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,black"
When SubType inherits SuperType through the prototype, SubType.prototype becomes an instance of SuperType and thus gets its own colors property. This is similar to creating SubType.prototype.colors. The end result is that all instances of SubType share this colors property, and modifications to instance1.colors are also reflected in instance2.colors.
- The second problem is that subtype cannot pass arguments to the parent type’s constructor during instantiation. In fact, there’s no way to pass arguments to the parent constructor without affecting all object instances. Combined with the problem of reference values in prototypes, the prototype chain is rarely used alone.
Constructor Stealing
Basic idea: Call the parent constructor inside the child constructor.
Since functions are simple objects that execute code in a specific context, you can use apply() and call() to execute the constructor with the newly created object as the context.
function SuperType() {
this.colors = ['red', 'blue', 'green'];
}
function SubType() {
// Inherit 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"
By using call() (or apply()), the SuperType constructor is executed in the context of the new object created for the SubType instance. This is equivalent to running all initialization code from SuperType() on the new SubType object! This way, each instance has its own properties.
1. Passing Arguments
Constructor stealing allows passing arguments from the child constructor to the parent constructor.
function SuperType(name) {
this.name = name;
}
function SubType() {
// Inherit SuperType and pass arguments
SuperType.call(this, 'cosine');
// Instance property
this.age = 21;
}
let instance = new SubType();
console.log(instance.name); // "cosine";
console.log(instance.age); // 21
2. Main Problems
The main drawbacks of constructor stealing, which are also problems with the constructor pattern for custom types:
- Methods must be defined in the constructor, so functions cannot be reused
- Subclasses cannot access methods defined on the parent’s prototype, so all types can only use the constructor pattern
Due to these problems, constructor stealing is also rarely used alone.
Combination Inheritance
Combination inheritance combines the prototype chain and constructor stealing, bringing together the advantages of both.
- The basic idea is to use the prototype chain to inherit properties and methods on the prototype, and use constructor stealing to inherit instance properties
- Methods can be defined on the prototype for reuse, while each instance can have 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) {
// Inherit properties
SuperType.call(this, name);
this.age = age;
}
// Inherit methods
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function () {
console.log(this.age);
};
let instance1 = new SubType('cosine', 21);
instance1.colors.push('black');
console.log(instance1.colors); // "red,blue,green,black"
instance1.sayName(); // "cosine";
instance1.sayAge(); // 21
let instance2 = new SubType('NaHCOx', 22);
console.log(instance2.colors); // "red,blue,green"
instance2.sayName(); // "NaHCOx";
instance2.sayAge(); // 22
Prototypal Inheritance
Applicable scenario: You already have an object and want to create a new object based on it. You pass this object to object(), then make appropriate modifications to the returned object.
let person = {
name: 'Nicholas',
friends: ['Shelby', 'Court', 'Van'],
};
let anotherPerson = Object.create(person, {
name: {
value: 'Greg',
},
});
console.log(anotherPerson.name); // "Greg"
It’s well-suited for the following scenarios:
- No need to create a separate constructor
- Need to share information between objects
- Note: Reference values contained in properties will always be shared among related objects
Parasitic Inheritance
Parasitic inheritance is closely related to prototypal inheritance. Create a function that implements inheritance, enhances the object in some way, and then returns it. The basic parasitic inheritance pattern is as follows:
function createAnother(original) {
let clone = Object.create(original); // Call constructor to create a new object
clone.sayHi = function () {
console.log(`Hi! I am ${this.name}`);
};
return clone; // Return this object
}
let person = {
name: 'cosine',
friends: ['NaHCOx', 'Khat'],
};
let person2 = createAnother(person);
person2.name = 'CHxCOOH';
person2.sayHi(); // Hi! I am CHxCOOH
This example uses person as the source object, returning a new object enhanced with a sayHi function. It’s mainly suitable for scenarios that focus on the object and don’t care about constructors and types.
Note:
- Adding functions to objects through parasitic inheritance makes them difficult to reuse, similar to the constructor pattern.
Parasitic Combination Inheritance
Combination inheritance also has efficiency problems, as the parent constructor is always called twice:
- When creating the subclass prototype
- Inside the subclass constructor
function SuperType(name) {
this.name = name;
this.colors = ['red', 'blue', 'green'];
}
SuperType.prototype.sayName = function () {
console.log(this.name);
};
function SubType(name, age) {
// Inherit properties
SuperType.call(this, name); // Second call to parent constructor!
this.age = age;
}
// Inherit methods
SubType.prototype = new SuperType(); // First call to parent constructor!
SubType.prototype.sayAge = function () {
console.log(this.age);
};
Essentially, the subclass’s prototype ultimately needs to contain all instance properties of the parent class, so the subclass constructor just needs to overwrite its own prototype when executed.
The main idea of parasitic combination inheritance:
- Inherit properties through constructor stealing
- Inherit methods through a mixed prototype chain In other words, take the parent prototype and shadow the original constructor with one pointing to the subclass:
function inheritPrototype(subType, superType) {
let prototype = Object.create(superType.prototype); // Create a copy of the parent prototype
prototype.constructor = subType; // Recover the constructor lost from prototype rewriting
subType.prototype = prototype; // Assign the object
}
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function() {
console.log(this.name);
};
function SubType(name, age){
// Inherit properties
SuperType.call(this, name); // Second call to parent constructor!
this.age = age;
}
// Inherit methods
- SubType.prototype = new SuperType(); // First call to parent constructor!
+ inheritPrototype(SubType, SuperType); // Changed to call this function
SubType.prototype.sayAge = function() {
console.log(this.age);
};
This avoids unnecessary multiple calls to the parent constructor while preserving the prototype chain intact. It can be considered the best pattern for reference type inheritance.
喜欢的话,留下你的评论吧~