About the object
Objects can be created and enhanced at any time during code execution and are extremely dynamic.
The usual way to create a custom Object is to create a new instance of Object and then add properties and methods to it.
let person = new Object();
person.name = "jk";
person.age = 23;
person.job = "idol";
person.sayName = function(){
console.log(this.name);
};
Copy the code
Nowadays, object literals have become more popular:
// let person = {name: "jk", age: 23, job: "idol", sayName(){console.log(this.name); }};Copy the code
Type of property
ES uses internal features to describe the characteristics of properties, which are defined by the specification that implements the ENGINE for JS, so developers cannot access these features directly in JS.
To identify a feature as an internal feature, the specification uses two brackets to enclose the name of the feature, such as [[Enumerable]].
Properties fall into two categories: data properties and accessor properties.
1. Data attributes
Contains a location to hold data values. Values are read from and written to this location. Data attributes have four properties that describe their behavior:
[[64x]] : Indicates whether a property can be deleted and redefined through delete, whether its features can be modified, and whether it can be changed to an accessor property.
[[Enumerable]] : indicates whether you can return in a for-in loop.
[[Writable]] : indicates whether the value of an attribute can be modified.
By default, this property is true for all attributes directly defined on an object.
[[Value]] : contains the actual Value of the attribute. The default value for this feature is undefined.
To change the default properties of a property, you must use the object.defineProperty () method (probably not in most cases). This method takes three parameters: the object to which the attribute is to be added, the name of the attribute, and a descriptor object. The last parameter, a property on the descriptor object, can contain different, Enumerable, Writable, and Value, which matches any of the different features. Depending on the feature you want to modify, you can set one or more of these values.
let person = {}; Object.defineproperty (person, "name", {writable: false, // unmodifiable attribute value: "JJK"}); console.log(person.name); // "JJK" person.name = "lms"; console.log(person.name); // "JJK"Copy the code
Attempts to reassign a read-only attribute in non-strict mode are ignored; In strict mode, trying to give a value for a read-only attribute throws an error.
The same rules apply to creating a non-configurable property, named 64X: False. In addition, once a property is defined as unconfigurable, it cannot be changed back to configurable. Calling Object.defineProperty () again and modifying any non-writable properties results in an error.
let person = {}; DefineProperty (person, "name", {64x: false, // indicates the configurable property value: "JJK"}); DefineProperty (person, "name", {64X: true, value: "JJK"});Copy the code
When calling Object.defineProperty (), the different, Enumerable, and writable values are default to false if they are not specified.
2. Accessor properties
It is not necessary to include a getter and setter functions. There are four characteristics that describe their behavior:
[[64x]] : Indicates whether a property can be deleted and redefined through delete, and whether its features can be modified, and whether it can be changed to a data property.
[[Enumerable]] : indicates whether you can return in a for-in loop.
By default, this feature is true for all attributes directly defined on an object.
[[Get]] : Get function, called when reading property, default value is undefined.
[[Set]] : Setting function, called when writing properties, default value is undefined.
Accessor properties cannot be defined directly; object.defineProperty () must be used.
Let book = {year_ : 2017, // pseudo private member, the underscore in year_ is often used to indicate that this property is not intended to be accessed outside of the object method. Edition: 1 // Public member}; Object.defineProperty(book, "year", { get(){ return this.year_; }, set(newValue){ if(newValue > 2017){ this.year_ = newValue; this.edition += newValue - 2017; }}}); book.year = 2018; console.log(book.edtion); / / 2Copy the code
This is a classic use scenario for accessor properties, where setting a property value causes some other change to occur.
Both get and set functions do not have to be defined. Defining only the fetch function means that the property is read-only and attempts to modify the property are ignored, throwing errors in strict mode. Only one setting function property is unreadable, returning undefined in non-strict mode and throwing an error in strict mode.
In browsers that do not support Object. DefineProperty (), you cannot modify [[Erable]] or [[Enumerable]].
Defining multiple properties
ES provides the Object.defineProperties () method, which defines multiple properties at once with multiple descriptors. Two parameters are received: the object to which attributes are to be added or modified, and another descriptor object whose attributes correspond to the attributes to be modified or added.
let book = {}; Object.defineProperties(book, { year_ : { value : 2017 }, edition : { value : 1 }, year : { get(){ return this.year_; }, set(newValue){ if(newValue > 2017){ this.year_ = newValue; this.edition += newValue - 2017; }}}});Copy the code
Read properties of properties
Use Object. GetOwnPropertyDescriptor () method, which can obtain the attributes of the specified descriptor. Accepts two parameters: the object of the property and the name of the property whose descriptor is to be retrieved. The return value is an object that provides a different, Enumerable, get, and set property for the accessor property, and optionally provides a different, Enumerable, writeable, and value property for the data property.
let book = {}; Object.defineProperties(book, { year_ : { value : 2017 }, edition : { value : 1 }, year : {get: function () {return this.year_; }, set: function(newValue){if(newValue > 2017){this.year_ = newValue; this.edition += newValue - 2017; }}}}); let des1 = Object.getOwnPropertyDescriptor(book, "year_"); console.log(des1.value); // 2017 console.log(des1.configurable); // false console.log(typeof des1.get); // "undefined" let des2 = Object.getOwnPropertyDescriptor(book, "year"); console.log(des2.value); // undefined console.log(des2.configurable); // false console.log(typeof des2.get); // "function"Copy the code
ES 2017 added Object. GetOwnPropertyDescriptors () static method, will call on each has its own attribute Object. GetOwnPropertyDescriptor (), and return them in a new Object.
Merge objects
Copies all the local properties of the source object to the target object. ES6 provides the object.assign () method. Receives a target Object and one or more of the source Object as a parameter, then each source objects can be enumerated (Object) propertyEnumerable () returns true) and has its own (Object. The hasOwnProperty () returns true) attributes are copied to the target Object. Properties with strings and symbols as keys are copied.
let desc, result; // Multiple source objects dest = {}; result = Object.assign(dest, {a : 'foo'}, {b : 'bar'}); console.log(result); // { a: foo, b: bar }Copy the code
Object.assign() actually makes a shallow copy of each source Object. If multiple source objects have the same property, the last copied value is used.
let dest, src, result; // override attribute dest = {id: 'dest'}; result = Object.assign(dest, { id : 'src1', a : 'foo' }, { id : 'src2', b : 'bar'}); // Object.assign() overwrites the duplicate property console.log(result); // {id: src2, a: foo, b: bar} src = { a : {} }; Object.assign(dest , src); // Shallow replication means that only references to the object console.log(dest) are copied; // { a : {} } console.log(dest.a === src.a); // trueCopy the code
If an error occurs during assignment, the operation aborts and exits, throwing an error. The Assign method does not have the concept of “rolling back” previous assignments, so it is a method that may only partially copy.
let dest, src, result; dest = {}; SRC = {a: 'foo', get b(){// assign throws new Error() when calling the fetch function; }, c: 'bar'}; try{ Object.assign(dest, src); catch(e) {}; } // Complete only partial copy, abort assignment and exit, and throw error console.log(dest); // { a : foo }Copy the code
Object identification and equality determination
The new object.is () method in ES6, much like ===, must take two arguments.
Console. log(object. is(+0, -0)); // false console.log(Object.is(+0, 0)); // true console.log(Object.is(-0, 0)); // false console.log(Object.is(NAN, NAN)); // trueCopy the code
To check for more than two values, recursively use equality passing:
function checkFn(x, ... rest){ return Object.is(x, rest[0]) && (rest.length < 2 || checkFn(... rest)); }Copy the code
Enhanced object syntax
1. Shorthand for attribute values
Simply using the variable name (no more colons) is automatically interpreted as a property key with the same name. If no variable with the same name is found, a ReferenceError is raised.
let name = 'JJK';
let person = {
name
};
console.log(person.name); // { name : 'JJK' }
Copy the code
The code compressors keep attribute names between scopes in case references are not found:
function makePerson(name){
return{
name
};
}
let person = makePerson('lms');
console.log(person.name); // lms
Copy the code
2. Computable properties
Dynamic property assignment can be done in object literals. The object property key surrounded by parentheses tells the runtime to evaluate it as a JS expression rather than a string:
const nameKey = 'name';
const ageKey = 'age';
const jobKey = 'job';
let person = {
[nameKey] : 'cln',
[ageKey] : 20,
[jobKey] : 'singer'
};
console.log(person); // { name : 'cln', age : 20, job: 'singer'}
Copy the code
Because evaluated as JS expressions, all computable attributes can themselves be complex expressions that are evaluated at instantiation time:
const nameKey = 'name';
const ageKey = 'age';
const jobKey = 'job';
let uniqueToken = 0;
function getUniqueKey(key){
return '$(key)_$(uniqueToken++)';
}
let person = {
[getUniqueKey(nameKey)] : 'cln',
[getUniqueKey(ageKey)] : 20,
[getUniqueKey(jobKey)] : 'singer'
};
console.log(person); // { name_0 : 'cln', age_1 : 20, job_2: 'singer'}
Copy the code
Any error thrown in a computable property expression interrupts object creation. If the expression throws an error, the previously completed calculation cannot be rolled back.
3. Abbreviate method name
let person = { sayName(name){ console.log('My name is ${name}'); }}; person.sayName('jjk'); // My name is jjkCopy the code
The same applies to get and set functions:
get name(){
return this.name;
},
set name(name){
this.name_ = name;
}
Copy the code
Compatible with computable property keys:
const methodKey = 'sayName';
let person = {
[methodKey] (name){
console.log('My name is ${name}');
}
}
person.sayName('jjk'); // My name is jjk
Copy the code
Object Deconstruction [ES new]
One or more assignment operations can be implemented using nested data in a single statement, that is, object attribute assignments are implemented using destructions that match the object.
Destructuring allows you to declare multiple variables and perform multiple assignments simultaneously in a structure that resembles an object literal.
Let person = {name: 'JJK ', age: 23}; let { name : personName, age : personAge } = person; console.log(personName); // jjk console.log(personAge); Let {name, age} = person; console.log(name); // jjk console.log(age); / / 23Copy the code
Destruct assignments do not necessarily match the attributes of the object. Some attributes can be ignored when assigning, and if the referenced attribute does not exist, the value of the variable is undefined.
let { name , job } = person;
console.log(job); // undefined
Copy the code
It is also possible to define a default value while deconstructing the assignment, which applies to cases where the referenced property mentioned earlier does not exist in the source object:
let { name, job = 'Software enginner' } = person;
Copy the code
Destructuring internally uses ToObject () (not directly accessible in the runtime environment) to transform source data structures into objects. This means that in the context of object deconstruction, the original values are treated as objects. This also means null and undefined cannot be deconstructed (as defined by the ToObject method) or TypeError will be raised.
let { length } = 'lms';
console.log(length); // 3
let { constructor : c } = 4;
console.log(c === Number); // true
let { _ } = null; // TypeError
let { _ } = undefined; // TypeError
Copy the code
Destructuring does not require variables to be declared in destructuring expressions. However, if you are assigning to an already-declared variable, the assignment expression must be enclosed in a pair of parentheses.
let personName, personAge;
let person = {
name : 'jjk',
age : 23
};
( { name : personName, age : personAge } = person );
Copy the code
1. Nested deconstruction
Destructuring has no restrictions on referencing nested attributes or assignment targets, so object attributes can be copied by destructuring:
let person = { name : 'jjk', age : 23, job : { title : 'idol' } }; let personCopy = {}; { name : personCopy.name, age : personCopy.age, job : personCopy.job } = person; // Since a reference to an object is assigned to the personCopy, modifying the attributes of the Person. job object also affects the personCopy. person.job.title = 'dancer'; console.log(personCopy); // { name : 'jjk', age : 23, job : { title : 'dancer' } }Copy the code
Destruct assignments can use nested structures to match nested attributes:
// Declare the title variable and assign the value of Person.job.title to it. let { job : { title } } = person; console.log(title); // idolCopy the code
Nested destructions cannot be used when the outer attributes are not defined, regardless of the source or target object.
- Part of the deconstruction
If a deconstructed expression involves multiple assignments, the initial assignment succeeds and the subsequent assignment fails, then only part of the deconstructed assignment will be completed.
- Parameter context matching
Destructuring assignments can also be done in function argument lists. Destructuring assignments to arguments does not affect the arguments object, but can be declared in the function signature to use local variables in the function body:
let person = {
name : 'jjk',
age : 23
};
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(name, age);
}
printPerson('1st', person, '2nd');
// ['1st', { name : 'jjk', age : 23 }, '2nd']
// 'jjk', 23
printPerson2('1st', person, '2nd');
// ['1st', { name : 'jjk', age : 23 }, '2nd']
// 'jjk', 23
Copy the code
Create an object
ES6 starts to officially support classes and inheritance.
Factory mode [rarely used since the advent of the constructor mode]
Used to abstract the process of creating a particular object. A way of creating objects based on a particular interface:
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("JJK",23,"IDOL");
let person2 = createPerson("cln",20,"singer")
Copy the code
While this factory pattern solves the problem of creating multiple similar objects, it does not solve the problem of object identification (what type the newly created object is).
Constructor pattern
The previous example was written using the 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("JJK",23,"IDOL");
let perso2 = new Person("cln",20,"singer");
person1.sayName(); // JJK
person2.sayName(); // cln
Copy the code
The difference is that the object is not explicitly created. Properties and methods are assigned to this without return.
By convention, constructor names begin with a capital letter, and non-constructors begin with a lowercase letter. The constructors of ES are functions that create objects.
The instanceof operator is generally more reliable for determining Object types; each Object in the previous example is an instanceof Object as well as an instanceof Person.
console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true
Defining custom constructors ensures that instances are identified as specific types, which is a big advantage over the factory pattern.
It doesn’t have to be a function declaration. Function expressions assigned to variables can also represent constructors:
let Person = function(name,age,job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
console.log(this.name);
};
}
Copy the code
When instantiating, if you do not want to pass parameters, the parentheses after the constructor are optional. Whenever you have the new operator, you can tune the corresponding constructor.
- Constructors are also functions
Any function called with the new operator is a constructor, and any function called without the new operator is a normal function.
// Call Person(" LMS ", 21, "student") as a function; // Add to the window object window.sayname (); // "lms"Copy the code
In cases where a function is called without explicitly setting this (that is, no method is called as an object, or no call is made with call () /apply ()), this always refers to the Global object (which in browsers is the Window object).
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 a prototype of the object created by calling the constructor.
The advantage of using a stereotype object is that properties and methods defined on it can be shared by the object instance.
function Person(){} Person.prototype.name = "JK"; // All attributes and the sayName () method are added directly to the Person prototype property. Person.prototype.age = 23; Person.prototype.job = "writer"; Person.prototype.sayName = function (){ console.log(this.name); }; let p1 = new Person(); let p2 = new Person(); console.log(p1.sayName == p2.sayName()); // trueCopy the code
Function expressions can also be used:
let person = function() {}; Person.prototype.name = "JK"; .Copy the code
- To understand the prototype
Whenever a function is created, a Prototype attribute (pointing to the prototype object) is created for the function according to specific rules. By default, all stereotype objects automatically get a property called constructor that refers back to the constructor associated with them.
Each time the constructor is called to create a new instance, the inner [[Prototype]] pointer of that instance is assigned to the constructor’s Prototype object.
There is no standard way to access the [[Prototype]] feature in scripts, but Firefox, Safari, and Chrome expose the __proto__ property on each object, which allows access to the Prototype of the object.
The instance is linked to the Prototype object by __proto__, which actually points to the hidden property [[Prototype]]; The constructor is linked to the prototype object via the Prototype property.
There is a direct connection between the instance and the constructor stereotype, but not between the instance and the constructor.
The constructor has a prototype property that references its prototype object, and the prototype object has a constructor property that references the constructor, i.e. the two are referenced in a loop. Function Person() {} the constructor has a console.log(typeof person.prototype) object associated with it; console.logPerson.prototype); // {// constructor: f Person(), // __proto__: The Object //} // constructor has a protyoType property that references its prototype Object, which in turn has a constructor property that references the constructor, i.e. console.log(Person.protptype.constructor === Person); //true // Normal prototype chain terminates at the prototype Object of Object, the prototype of Object 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 console.log(Person.prototype.__proto__); // { // constructor : f Object(), // toString : ... / / hasOwnProperty:... / /... // } let person1 = new Person(), person2 = new Person(); Console. log(person1! == Person); // true console.log(person1 ! == Person.prototype); // true console.log(Person.prototype ! == Person); // true console.log(person1.__proto__ === Person.prototype); // true console.log(person1.__proto.construtor === Person); // true where the instance is linked to the prototype object via __proto__, // Two instances created from the same constructor share the same prototype object console.log(person1.__proto__ === person2.__proto__); // true // instanceof checks if the instance's prototype chain contains the prototype console.log(person1 instanceof Person) specifying the constructor; // true console.log(person1 instanceof Object); // true console.log(Person.prototype instanceof Object); // trueCopy the code
This relationship between two objects can be determined using the isPrototypeOf() method. Essentially, this method returns true if the [[Prototype]] of the passed argument points to the object on which it was called.
console.log(Person.prototype.isPrototypeOf(person1)); // true
console.log(Person.prototype.isPrototypeOf(person2)); // true
Copy the code
ES Object has a method called Object.getProtoTypeof () that returns the value of the internal property of the parameter [[Prototype]] :
console.log(Object.getPrototypeOf(person1) == Person.prototype); // true
console.log(Object.getPrototypeOf(person1).name); // "JJK"
Copy the code
This method makes it easy to get a prototype of an object, which is especially important when implementing inheritance through prototypes.
The Object type also has a setPrototypeOf () method that writes a new value to an instance’s private property [[Prototype]] and overrides an Object’s Prototype inheritance. Using this approach can seriously affect code performance.
To avoid the potential performance degradation caused by using Object.setPrototypeof (), you can create a new Object with object.create () and specify a prototype for it:
let biped = { numLegs: 2 }; let person = Object.setPrototypeOf(biped); person.name = "cln"; console.log(Object.getPrototypeOf(person) === biped); // true console.log(person.numLengs); / / 2Copy the code
- Prototype level
The search begins with the object instance itself. If a given name is found on this instance, the value of that name is returned. If not, the search follows the pointer into the prototype object and returns the corresponding value when the property is found on the prototype object.
The constructor property mentioned earlier exists only in the prototype object and is therefore also accessible through the instance object.
While it is possible to read values on a prototype object from an instance, it is not possible to override them from an instance. If you add a property on the instance with the same name as the property on the prototype object, the property is created on the instance, and the property obsures the property on the prototype object.
Function Person () {} Person. Prototype. name = "JJK "; Person.prototype.age = 23; Person.prototype.sayName = function(){ console.log(this.name); }; let person1 = new Person(),person2 = new Person(); person1.name = "cln"; console.log(person1.name); // "CLN", from instance console.log(person2.name); // "JJK", from prototypeCopy the code
You can use the DELETE operator to completely remove attributes added on the instance, allowing the identifier resolution process to continue searching the prototype object.
delete person1.name; console.log(person1.name); // "JJK ", from prototypeCopy the code
The hasOwnProperty () method, used to determine whether a property is on an instance or a prototype object, returns true if the property exists on the instance of the object calling it.
console.log(person1.hasOwnProperty("name")); // false where name= "JJK" from the prototype person1.name = "CLN "; // Name comes from instance console.log(person1.hasownProperty ("name")); // true delete person1.name; // Name = "JJK", from the prototype console.log(person1.hasownProperty ("name")); // falseCopy the code
* * ES Object. GetOwnPropertyDescriptor () method is only effective for instance attributes. To achieve a stereotype property descriptor, it is necessary to direct calls on the prototype Object Object. GetOwnPropertyDescriptor (). 支那
- Stereotype and in operator
There are two ways to use the IN operator: alone and in for-in loops. When used alone, the IN operator returns true when the specified property is accessible through the object, whether it is on an instance or a stereotype.
If you want to determine whether an attribute exists on a stereotype, you can use both the hasOwnProperty () and in operators:
function hasPrototypeProperty(object, name){ return ! Object.hasownproperty (name) && (name in object); }Copy the code
The in operator returns true as long as it is accessible through the object. HasOwnProperty () returns true only if the property exists on the instance. So as long as the in operator returns true and hasOwnProperty () returns false, the property is a stereotype property.
HasPrototypeProperty () is used to determine whether the property is on the stereotype.
let person = new Person(); console.log(hasPrototypeProperty(person, "name")); Person. name = "LMS "; person.name =" LMS "; // At this point, the property is overridden on the instance, which also has the property, and the property on the instance overshadows the property on the prototype. console.log(hasPrototypeProperty(person, "name")); // falseCopy the code
When you use the IN operator in a for-in loop, all properties that can be accessed through an object and can be enumerated are returned, including instance and stereotype properties. Instance properties that are not Enumerable in the masking stereotype ([[Enumerable]] property is set to false) will also be returned in the for-in loop because all properties defined by default are Enumerable.
To get all enumerable instance properties on an Object, use the object.keys () method. Takes an object as an argument and returns an array of strings containing the names of all the enumerable properties of the object.
let keys = Object.keys(Person.prototype);
console.log(keys); // "name,age,job,sayName"
let p1 = new Person();
p1.name = "jk";
p1.age = 20;
let p1keys = Object.keys(p1);
console.log(p1keys); // "[name, age]"
Copy the code
Want to list all instance attributes, whether or not can be enumerated, the Object can be used. The getOwnPropertyNames (). The returned array contains a non-enumerable property constructor. Both methods can be used in place of for-in loops when appropriate.
After the ES6 new symbols, accordingly increase the Object. A method getOwnPropertySymbols (), just for symbol:
let k1= Symbol('k1'), k2 = Symbol('k2');
let o = {
[k1] : 'k1',
[k2] : 'k2'
};
console.log(Object.getOwnPropertySymbols(o));
// [Symbol(k1),Symbol(k2) ]
Copy the code
- Attribute enumeration order
The enumeration order of for-in loops and object.keys () is uncertain, depends on the JavaScript engine, and may vary from browser to browser.
Object. GetOwnPropertyNames (), Object. GetOwnPropertySymbols () and the Object, the assign () enumeration sequence is deterministic. Enumerates numeric keys in ascending order, and then enumerates strings and symbolic keys in insertion order.
let k1 = Symbol('k1'), k2 = Symbol('k2'); let o = { 1 : 1, first : 'first', [k1] : 'sym2', second : 'second', 0 : 0 }; o[k2] = 'sym2'; // Add the enumeration element 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
Object iteration
ES2017 added two static methods: both accept an Object, object.values () returns an array of Object values, and object.entries () returns an array of key/value pairs.
Non-string attributes are converted to string output. Both methods perform a shallow copy of the object. And symbolic attributes are ignored:
const = { qux : {} }; console.log(Object.values(o) [0] === o.qux); // true console.log(Object.entries(o) [0] [1] === o.qux); // true const sym = Symbol(); const a = { [sym] : 'foo' }; console.log(Object.values(a)); // [] console.log(Object.entries(a)); / / []Copy the code
- Other prototype syntax
Rewrite the prototype of the previous example with an object literal containing all properties and methods:
function Person(){} Person.prototype = { constructor : Person, name : "jk", age : 23, job : "idol", sayName(){ console.log(this.name); }};Copy the code
Note that restoring the constructor property this way creates a property that [[Enumerable]] is true, whereas the native constructor property is not Enumerable by default.
If you are using an ES compatible JS engine, you can define the constructor property using the object.defineProperty () method instead:
function Person(){} Person.prototype = { name : "jk", age : 23, job : "idol", sayName(){ console.log(this.name); }}; Object. DefineProperty (Person. Prototype, "constructor"){enumerable: false, value: Person};Copy the code
- The dynamics of prototypes
Any changes made to the prototype object are reflected in the instance, even if the instance existed before modifying the prototype.
While you can add attributes and methods to your prototype at any time and immediately reflect them on all object instances, it’s not the same thing as recreating the entire prototype. Instance’s [[Prototype]] pointer, which is automatically assigned when the constructor is called, does not change even when the Prototype is changed to a different object. Rewriting the entire stereotype disconnects the original stereotype from the constructor, but the instance still references the original stereotype. Remember that instances only have Pointers to prototypes, not to constructors.
function Person(){} let friend = new Person(); Person.prototype = { name : "jk", age : 23, job : "idol", sayName(){ console.log(this.name); }}; friend.sayName(); // Error because the friend instance references the original prototype and does not have the sayName () methodCopy the code
Instances created after reconstructing the stereotype on the function reference the new stereotype. Instances created before this point will still reference the original prototype.
- Native object prototype
The stereotype pattern is important for custom types, and it is also the pattern that implements all native reference types. All constructors of native reference types (Object, Array, String, and so on) define instance methods on stereotypes. Sort () for Array instances is defined on array.prototype, and substring () for string-wrapped objects is defined on String.prototype.
You can get references to all default methods from a prototype of a native object, and you can define new methods for instances of a native type.
String.prototype.startsWith = function(text){ return this.indexOf(text) === 0; }; let msg = "Hello world!" ; console.log(msg.startsWith("Hello")); // trueCopy the code
It is not recommended to modify native object prototypes in a production environment, as naming conflicts may occur. It is recommended to create a custom class that inherits the native type.
- Problems with prototypes
All attributes on the stereotype are shared between instances, which is appropriate for functions.
In general, different instances should have their own copies of attributes, which is why prototype patterns are not usually used in isolation in real development.
inheritance
Many object-oriented languages support two types of inheritance: interface inheritance, which inherits only the front of a method, and implementation inheritance, which inherits the actual method. Interface inheritance is not possible in ES because functions have no signature. Implementation inheritance is the only inheritance method supported by ES, mainly through the prototype chain.
Prototype chain
Basic idea: Inherit 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 constructor that refers back to the constructor, and the instance has an internal pointer to the stereotype.
If the stereotype is an instance of another type, that means that the stereotype itself has an internal pointer to another stereotype, which in turn has a pointer to another constructor. This creates a prototype chain between the instance and the prototype, which is the basic idea of a prototype chain.
function SuperType(){ this.property = true; } SuperType.prototype.getSuperValue = function(){ return this.property; }; function SubType(){ this.subproperty = false; } SubType.prototype = new SuperType(); / / inheritance SuperType SuperType. Prototype. GetSubValue = function () {return enclosing subproperty; }; let instance = new SubType(); console.log(instance.getSuperValue); // trueCopy the code
Instead of using the default stereotype, SubType replaces it with a new object that happens to be an instance of SuperType. In this way, instances of SubType can not only inherit properties and methods from instances of SuperType, but can also be linked to the SuperType stereotype.
Note that getSuperValue () is a prototype method and Property is an instance property, so the property is stored on subtype.prototype. Since the constructor property of subtype. prototype was overridden to point to SuperType, instance.constructor also points to SuperType.
Prototype chains extend the previous prototype search mechanism. The search for properties and methods goes all the way to the end of the prototype chain.
- The default prototype
By default, all reference types inherit from Object, which is also implemented through the stereotype chain. The default prototype for any function is an instance of Object, meaning that the instance has an internal pointer to Object.prototype.
SubType inherits from SuperType, and SuperType inherits from Object. When you call instance.tostring (), you’re actually calling the method saved on Object.prototype.
- Archetype and inheritance
The relationship between stereotypes and instances is determined in two ways: the instanceof operator Returns true if the corresponding constructor is present in the stereotype chain of an instance.
instance instanceof SubType // true
instance instanceof SuperType // true
instance instanceof Object // true
Copy the code
The isPrototypeOf() method can be called by each prototype in the prototype chain, and returns true as long as the prototype chain contains the prototype.
Object.prototype.isPrototypeOf(instance) // true
SuperType.prototype.isPrototypeOf(instance) // true
SubType.prototype.isPrototypeOf(instance) // true
Copy the code
- About the way
Subclasses sometimes need to override methods of the parent class, or add methods that the parent class does not have. These methods must be added to the stereotype after the stereotype is assigned.
function SuperType(){ this.property = true; } SuperType.prototype.getSuperValue = function(){ return this.property; }; function SubType(){ this.subproperty = false; } SubType.prototype = new SuperType(); / / inheritance SuperType SuperType. / / new method. The prototype getSubValue = function () {return enclosing subproperty; }; / / cover existing methods SubType. Prototype. GetSuperValue = function () {return false. } let instance = new SubType(); console.log(instance.getSuperValue); // falseCopy the code
The point is that both methods are defined after the stereotype is assigned to an instance of SuperType.
Creating a prototype as an object literal breaks the previous prototype chain, essentially rewriting it.
// SuperType subtype. prototype = new SuperType(); Subtype.prototype = {getSubValue(){return this.subproperty; }; someMethods(){ return false; }}; let instance = new SubType(); console.log(instance.getSuperValue); // Error!!Copy the code
The overwritten stereotype is an instance of Object, not SuperType, so the previous stereotype chain is broken.
- The prototype chain problem
Because the reference values contained in the stereotype are shared across all instances, when inheritance is implemented using the stereotype, the stereotype actually becomes an instance of another type, meaning that the original instance properties become stereotype properties.
The second problem is that a subtype cannot take arguments to the parent type’s constructor when instantiated.
Due to the above problems, the original chain will not be used alone.
Stolen constructors (also known as “object camouflage” or “classical inheritance”)
A technique for solving inheritance problems caused by stereotypes containing reference values. Basic idea: call the superclass constructor in the subclass constructor. Because functions are simple objects that execute code in a particular context, you can use the apply () and call () methods to execute constructors in the context of the newly created object.
Function SuperType(){this.colors = ["red", "blue"]; } function SubType(){ SuperType.call(this); SuperType} let a = new SubType(); a.colors.push("green"); console.log(a.colors); // "red,blue,green" let b = new SubType(); console.log(b.colors); // "red,blue"Copy the code
- Passing parameters
One advantage of using a stolen constructor over a prototype chain is that you can pass arguments to the superclass constructor in the subclass constructor.
function SuperType(name){ this.name = name; } function SubType(){ SuperType.call(this, "jk"); // this. Age = 22; } let a = new SubType(); console.log(a.name); // "jk" console.log(a.age); / / 22Copy the code
- Problem with embezzling constructors
Methods must be defined in constructors, so functions cannot be reused.
Subclasses do not have access to methods defined on superclass prototypes, so all types can only use the constructor pattern.
Because of the above problems, the stolen constructor is almost never used alone.
Combinatorial inheritance (also known as “pseudo-classical inheritance”)The most used inheritance pattern】
The basic idea is to use the prototype chain to inherit the properties and methods on the prototype, and to inherit the instance properties through the stolen 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); this.age = age; } SubType.prototype = new SuperType(); Subtype.prototype.sayage = function(){console.log(this.age); } let a = new SubType("lms",21); a.colors.push("grey"); console.log(a.colors); // "red,blue,green,grey" a.sayName(); // "lms" a.sayAge(); // 21 let b = new SubType("jjk",23); console.log(b.colors); // "red,blue,green" b.sayName(); // "jk" b.sayAge(); / / 23Copy the code
Composite inheritance makes up for the deficiency of prototype chain and embeded constructor, and is the most used inheritance method in JS.
Primary inheritance
Information can be shared between objects through stereotypes, even without custom types.
let person = {
name : "jk",
friends : ["v","jimin"]
};
let person1 = object(person);
person1.name = "Mary";
person1.friends.push("john");
let person2 = object(person);
person2.name = "Jack";
person2.friends.push("cln");
console.log(person.friends); // "v, jimin, john, cln"
Copy the code
Primitive inheritance works in this case: you have an object and want to create a new object based on it. You need to pass the object to object () and then modify the returned object appropriately.
ES5 normalizes the concept of old-style inheritance by adding the object.create () method. It takes two parameters: the object to be the prototype for the new object and, optionally, the object to define additional properties for the new object. Same effect as the object () method when there is only one argument. The second argument is the same as the second argument to Object.defineProperties () : each new property is described by its own descriptor, and any property added in this way overshadows any property of the same name on the prototype Object.
let person = {
name : "jk",
friends : ["v","jimin"]
};
let person1 = object.create(person, {
name : {
value : "cln"
}
});
console.log(person1.name); // "cln"
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. ** But keep in mind that reference values contained in attributes are always shared between related objects, just as with the stereotype pattern.
Parasitic inheritance
It is close to the original type inheritance. The basic idea: Create a function that implements inheritance, enhances the object in some way, and then returns the object.
Function createAnother(original){let clone = object(original); SayHi = function(){// Somehow enhance this object console.log("hi"); } return clone; } let person = { name : "jk", friends : ["v","jimin"] }; let person1 = createAnother(person); person1.sayHi(); // "hi"Copy the code
The same applies to scenarios where the main focus is on objects, not types and constructors. The object () function is not required for parasitic inheritance, and any function that returns a new object can be used here.
Adding functions to objects in this way makes them difficult to reuse, similar to the constructor pattern.
Parasitic combinatorial inheritance
Inheriting properties by stealing constructors, but using a hybrid prototype chain inheritance approach.
The basic idea is to get a copy of the parent stereotype, using parasitic inheritance to inherit the parent stereotype, and then assign the new object returned to the child stereotype.
function inheritPrototype(subType, superType){ let prototype = object(superType.prototype); Prototype. constructor = SubType; // Add object subtype. prototype = prototype; // Assign object}Copy the code
InheritPrototype () takes two parameters: a subclass constructor and a superclass constructor. Inside this function, the first step is to create a copy of the parent prototype, then set the constructor attribute to the returned Prototype object pointing to the subclass constructor, and finally assign the newly created object to the subtype 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);
};
Copy the code
The SuperType constructor is called only once, avoiding unnecessary and unnecessary attributes on the SubType stereotype. This example is more efficient, and the original chain remains the same. Parasitic combinatorial inheritance is the best model for reference type inheritance.
Class [ES6 added]
The new class keyword introduced in ES6 has the ability to formally define classes. Class is the new basic syntactic sugar structure in ES. Behind the scenes are still the concepts of stereotypes and constructors.
The class definition
Similar to function types, classes are defined in two main ways:
- Class declaration
class Person{}
- Such expressions
const Animal = class{};
Like function expressions, class expressions cannot be referenced until they are evaluated. But unlike function definitions, although function declarations can be promoted, class definitions cannot (that is, classes are used after they are defined).
console.log(FunctionDeclaration); // FunctionDeclaration(){}
function FunctionDeclaration(){};
console.log(FunctionDeclaration); // FunctionDeclaration(){}
console.log(ClassDeclaration); // ReferenceError: ClassDeclaration is not defined
class ClassDeclaration{};
console.log(ClassDeclaration); // ClassDeclaration(){}
Copy the code
Another difference from function declarations is that functions are scoped by functions and classes are scoped by blocks
{
function FunctionDeclaration() {}
class ClassDeclaration {}
}
console.log(FunctionDeclaration); // FunctionDeclaration(){}
console.log(ClassDeclaration); // ReferenceError: ClassDeclaration is not defined
Copy the code
Classes can contain constructor methods, instance methods, get functions, set functions, and static class methods, but these are not required. It is recommended that the first letter of the class name be capitalized.
Class constructor
The definition of the constructor is not required. It is better to define the constructor as an empty function without defining the constructor.
- instantiation
The constructor keyword is used to create class constructors within the class definition block.
class Person {
constructor(){
console.log('person ctor');
}
}
let p = new Person(); // person ctor
Copy the code
The arguments passed in when the class is instantiated are used as arguments to the constructor. If no arguments are required, the parentheses following the class name are optional.
The main difference between class constructors and constructors is that class constructors must be called using the new operator; Normally, if new is not used, the global this (usually window) is used as the internal object. An error is thrown if you forget to use new when calling a class constructor.
There is nothing special about a class constructor; after instantiation, it becomes a normal instance method. Once instantiated, it can be referenced on the instance:
class Person{} let p1 = new Person{}; p1.constructor(); // TypeError let p2 = new p1.constructor(); // Create a new instance using a reference to the class constructorCopy the code
- Think of classes as special functions
ES has no formal class type. The ES class is a special kind of function in many ways. After declaring a class, the class identifier is detected through the Typeof operator to indicate that it is a function:
class Person{}
console.log(Person); // class Person{}
console.log(typeof Person); // function
Copy the code
The class identifier has a Prototype attribute, which also has a constructor attribute pointing to the class itself:
The class Person {} the console. The log (Person) prototype); / / {constructor: f ()}. The console log (Person) prototype) constructor = = = Person); // trueCopy the code
Like normal constructors, you can use the instanceof operator to examine an object and a class constructor to determine if the object is an instanceof a class.
A class can be passed as an argument just like any other object or function reference:
// Classes can be defined anywhere just like functions, Let classList = [class{constructor(id){this.id_=id; console.log('instance ${this.id_}');}}]; let classList = [class{constructor(id){this.id_=id; console. function createInstance(classDefination, id){ return new classDefination(id); } let foo = createInstance(classList[0], 3141); // instance 3141Copy the code
Similar to calling function expressions immediately, classes can be instantiated immediately:
Let p = new class Foo{constructor(x){console.log(x); let p = new class Foo{constructor(x){console.log(x); } }('bar'); // bar console.log(p); // Foo{}Copy the code
Instances, stereotypes, and class members
1. Instance member
The class constructor is executed each time the class identifier is called through new. Inside this function, you can add “own” attributes to the newly created instance (this). After the constructor completes, you can still add new members to the instance. Each instance corresponds to a unique member object, meaning that none of the members are shared on the stereotype:
class Person{
constructor(){
this.name = new String('Jack');
this.sayName = () => console.log(this.name);
this.nicknames = ['Jake', 'J-Dog']
}
}
let p1 = new Person{},p2 = new Person{};
p1.sayName(); // Jack
p2.sayName(); // Jack
console.log(p1.name === p2.name); // false
console.log(p1.sayName === p2.sayName); // false
console.log(p1.nicknames === p2.nicknames); // false
p1.name = p1.nicknames[0];
p2.name = p2.nicknames[1];
p1.sayName(); // Jake
p2.sayName(); // J-Dog
Copy the code
2. Prototype methods and accessors
To share methods between instances, the class definition syntax treats methods defined in a class block as prototype methods.
class Person{ constructor(){ this.locate = () => console.log('instance'); } locate(){console.log('prototype');} // Locate (){console.log('prototype'); } } let p = new Person(); p.locate(); // instance Person.prototype.locate(); // prototypeCopy the code
You can define methods in class constructors/class blocks, but you cannot add primitive values/objects to prototypes as member data in class blocks:
class Person{
name : 'jjk' // Uncaught SyntaxError
}
Copy the code
Class methods are equivalent to object properties, so strings, symbols, or computed values can be used as keys:
const symbolKey = Symbol('sym');
class Person{
[symbolKey](){
console.log('invoked symbolKey');
}
['computed' + 'Key'](){
console.log('invoked computedKey');
}
}
let p = new Person();
p.symbolKey(); // invoked symbolKey
p.computedKey(); // invoked computedKey
Copy the code
Class definitions also support getting and setting accessors, with the same syntax and behavior as ordinary objects.
class Person{ set name(newName){ this.name_ = name; } get name(){ return this.name_; }}Copy the code
Static class methods
Typically used to perform operations that are not instance-specific and do not require the existence of an instance of a class. Like prototype members, static members can only have one on each class.
Class Person{constructor(){this.locate = () => console.log('instance', this); } locate(){console.log('prototype', this); } static locate(){console.log('class', this); }}Copy the code
Static class methods are great for instance factories:
Static create(){// Create and return a Person instance with a random age return new Person(math.floor (math.random ()*100)); }Copy the code
4. Non-function archetypes and class members
While the class definition does not explicitly support adding member data to a stereotype or class, it is possible to add it manually outside the class definition:
class Person{ sayName(){ console.log('$(Person.greeting) $(this.name)'); } } Person.greeting = 'My name is'; // Define the data member on the class person.prototype. name = 'CLN'; // Define the data member on the prototype let p = new Person(); p.sayName(); // My name is CLNCopy the code
Iterator and generator methods
The class definition syntax supports defining generator methods on both prototypes and classes themselves.
Class Person{*createNicknameIterator(){yield 'nn' on the prototype; yield 'jk'; yield 'cln'; } static *createJobIterator(){yield 'idol'; yield 'teacher'; yield 'boss'; } } let job = Person.createJobIterator(); console.log(job.next().value); // idol console.log(job.next().value); // teacher console.log(job.next().value); // boss let p = new Person(); let nick = p.createNicknameIterator(); console.log(nick.next().value); // nn console.log(nick.next().value); // jk console.log(nick.next().value); // clnCopy the code
Because generator methods are supported, class instances can be made iterable by adding a default iterator:
class Person{
constructor(){
this.nicknames = ['nn', 'jk', 'cln'];
}
*[Symbol.iterator]{
yield *this.nicknames.entries();
}
}
let p = new Person();
for(let [idx, nickname] of p){
console.log(nickname);
}
Copy the code
You can also return only iterator instances
class Person{ constructor(){ this.nicknames = ['nn', 'jk', 'cln']; } [Symbol.iterator]{ yield this.nicknames.entries(); }}Copy the code
inheritance
Although class inheritance uses the new syntax, it still uses the stereotype chain behind it.
- Inheritance based
ES6 classes support single inheritance. Using the extends keyword, you can inherit any object that has [[Construct]] and a stereotype. For the most part, this means that you can inherit not only from a class, but also from ordinary constructors (maintaining backward compatibility).
Function Person(){} class Engineer extends Person(){} class Engineer extends Person(){ e = new Engineer(); console.log(e instanceof Engineer); // true console.log(e instanceof Person); // trueCopy the code
Derived classes access the class and methods defined on the stereotype through the stereotype chain. The value of this reflects the instance or class calling the corresponding method:
Class Vehicle{identifyPrototype(id){console.log(id, this); } static identifyClass(id){console.log(id, this); } } class Bus extends Vehicle{} let v = new Vehicle{}; let b = new Bus{}; b.identifyPrototype('bus'); // bus, Bus{} v.identifyClass('vehicle'); // vehicle, Vehicle{} Bus.identifyPrototype('bus'); // bus, class Bus{} Vehicle.identifyClass('vehicle'); // vehicle, class Vehicle{}Copy the code
The extends keyword can also be used in class expressions, so let Bar = class extends Foo{} is valid.
- Constructors, HomeObject, and super ()
Methods of derived classes can reference their archetypes through the super keyword, and can only be used within derived classes, and only within class constructors, instance methods, and static methods. Using super in the class constructor calls the superclass constructor.
class Vehicle{ constructor(){ this.hasEngine = true; }} Class Bus extends Vehicle{constructor(){// Do not refer to this before calling super () or else raise ReferenceError. Super (); console.log(this instanceof Vehicle); // true console.log(this); // Bus{ hasEngine = true } } } new Bus{};Copy the code
Static methods defined on inherited classes can be called via super in static methods:
class Vehicle{
static identify(){
console.log('vehicle static function');
}
}
class Bus extends Vehicle{
static identify(){
super.identify();
}
}
Bus.identify(); // vehicle static function
Copy the code
ES6 adds an internal property [[HomeObject]] to class constructors and static methods, which is a pointer to the object that defines the method. This pointer is automatically assigned and can only be accessed from within the JS engine. Super is always defined as a prototype for [[HomeObject]].
Note when using super:
-
Super can only be used in derived class constructors and static methods;
-
The super keyword cannot be used alone, either by calling constructors or by referring to static methods;
constructor(){
console.log(super);
// SyntaxError : 'super' keyword unexpected here
}
Copy the code
-
Calling super () calls the superclass constructor and assigns the returned instance to this;
-
Super () behaves like a constructor call; if you need to pass arguments to the parent constructor, you need to pass them manually.
class Vehicle{
constructor(license){
this.license = license;
}
}
class Bus extends Vehicle{
constructor(license){
super(license);
}
}
console.log(new Bus('d8v2d')); // Bus{ license: 'd8v2d' }
Copy the code
- If the class constructor is not defined, super () is called when the derived class is instantiated and all arguments passed to the derived class are passed.
class Vehicle{
constructor(license){
this.license = license;
}
}
class Bus extends Vehicle{}
console.log(new Bus('d8v2d')); // Bus{ license: 'd8v2d' }
Copy the code
-
In class constructors, you cannot refer to this before calling super ().
-
If a constructor is defined in a derived class, then either super () must be called in it or an object must be returned in it.
class Vehicle{}
class Car extends Vehicle{}
class Bus extends Vehicle{
constructor(){
super();
}
}
class Van extends Vehicle{
constructor(){
return {};
}
}
console.log(new Car()); // Car{}
console.log(new Bus()); // Bus{}
console.log(new Van()) // {}
Copy the code
- Abstract base class
Inherited by other classes, but not instantiated itself. There is no syntax in ES that specifically supports this class, but it can be done through new.target. New.target holds classes or functions called through the new keyword. You can prevent instantiation of an abstract base class by detecting whether new.target is an abstract base class at instantiation time.
Class Vehicle{// Abstract base class constructor(){console.log(new.target); if(new.target === Vehicle){ throw new Error('Vehicle cannot be directly instantiated'); } } } class Bus extends Vehicle{}; // Derived class new Bus(); // class Bus{} new Vehicle(); // class Vehicle{} // Error: Vehicle cannot be directly instantiatedCopy the code
By checking in the abstract base class constructor, you can require that a derived class must define a method.
class Vehicle{ constructor(){ if(new.target === Vehicle){ throw new Error('Vehicle cannot be directly instantiated'); } if(! this.foo){ throw new Error('Inheriting class must define foo()'); } console.log('success!! '); }} class Bus extends Vehicle{foo(){}} class Van extends Vehicle{} new Bus(); // success!! new Van(); // Error: Inheriting class must define foo()Copy the code
- Inheriting built-in types
class SuperArray extends Array{
....
}
Copy the code
Some methods of built-in types return new instances. By default, the type of the returned instance is the same as that of the original instance. To override this default behavior, you can override the symbol.species accessor, which determines the class to use when creating the returned instance.
class SuperArray extends Array{
static get [Symbol.species](){
return Array;
}
}
let a1 = new SuperArray(1,2,3,4,5);
let a2 = a1.filter(x => !!(x%2));
console.log(a1 instanceof SuperArray); // true
console.log(a2 instanceof SuperArray); // false
Copy the code
- Class with
It is a common JS pattern to group different classes of behavior into a single class.
Note that the object.assign () method is designed to blend in Object behavior. It is only necessary to implement a mixin expression yourself if you need to mixin the behavior of the class. If you simply want to mix multiple Object attributes, use object.assign ().
The extends keyword is followed by a JavaScript expression. Any expression that can be resolved to a class or a constructor is valid. This expression is evaluated when the class definition is evaluated:
class Vehicle{} function getParentClass(){ console.log('evaluated expression'); return Vehicle; } class Bus extends getParentClass(){} // An evaluable expressionCopy the code
The mixin pattern can be implemented by concatenating multiple mixin elements in an expression that eventually resolves into a class that can be inherited.
The strategy for implementing this pattern is to define a set of “nested” functions, each of which takes a superclass as an argument, define the mixin class as a subclass of that argument, and return that class. These composite functions can be called sequentially to form superclass expressions:
class Vehicle{}
let FooMixin = (Superclass) => class extends Superclass{
foo(){
console.log('foo');
}
}
let BarMixin = (Superclass) => class extends Superclass{
bar(){
console.log('bar');
}
}
let ClnMixin = (Superclass) => class extends Superclass{
cln(){
console.log('cln');
}
}
class Bus extends FooMinxin(BarMixin(ClnMixin(Vehicle))) {}
let b = new Bus();
b.foo(); // foo
b.bar(); // bar
b.cln(); // cln
Copy the code
Many JS frameworks (React in particular) have abandoned mixin in favor of composition (extracting methods into separate classes and auxiliary objects and then combining them without using inheritance). The design principle of “composition beats inheritance” is widely followed and provides great flexibility when it comes to code.