Background introduction
JavaScript is a special kind of programming language. Unlike other programming languages, JavaScript can dynamically change the type of a variable at runtime.
For example, you would never know how many types a variable like isTimeout could have. In addition to Boolean values true and false, it could also be undefined, 1 and 0, a timestamp, or even an object.
If the code fails to run, open the browser and start debugging the breakpoint. InfoList is first assigned as an array:
[{name: 'test1', value: '11'}, {name: 'test2', value: '22'}]
Copy the code
After a while it turned into an object:
{test1:'11', test2: '22'}
Copy the code
In addition to variables that can be assigned to any type at runtime, inheritance can be implemented in JavaScript, but it is not class-based inheritance like programming languages such as Java, C++, and C#, but rather prototype-based inheritance.
This is because JavaScript has a special existence: objects. Each object also has a prototype object from which methods and properties can be inherited.
When it comes to objects and prototypes, there are the following problems:
-
How can a JavaScript function be an object?
-
How does Proto relate to Prototype?
-
How do objects implement inheritance in JavaScript?
-
How does JavaScript access object methods and properties?
The relationship between prototype objects and objects
In JavaScript, an object consists of one or more groups of properties and values:
{
key1: value1,
key2: value2,
key3: value3,
}
Copy the code
Objects are very versatile in JavaScript because their values can be primitive types (number, String, Boolean, NULL, undefined, Bigint, and symbol) as well as objects and functions.
Objects, functions, and arrays are all instances of Object, which means that in JavaScript everything is an Object except the primitive type.
Which brings us to question 1: How can a JavaScript function be an object?
In JavaScript, a function is a special object that also has properties and values. All functions have a special property, prototype, whose value is an object. This object is called a “prototype object”.
We can print this property on the console:
function Person(name) {
this.name = name;
}
console.log(Person.prototype);
Copy the code
The output is as follows:
As you can see, the prototype object has two properties: constructor and Proto.
Here, we seem to wonder “2: What is the relationship between Proto and Prototype?” The answer is coming. In JavaScript, the proto property points to the object’s prototype object, which in the case of a function is prototype. The prototype object of the function has the following characteristics:
-
By default, all function prototype objects have the constructor attribute, which points to the associated constructor, in this case the Person function;
-
The Person function’s prototype object also has its own prototype object, represented by the Proto attribute. As mentioned earlier, functions are instances of Object, so the prototype Object for Person.prototype is Object.prototype.
We can use this diagram to describe the relationship between the prototype, proto, and constructor properties:
From this graph, we can find the following relationship:
-
In JavaScript, the proto property points to the object’s prototype object;
-
For functions, each function has a prototype property, which is the prototype object of that function;
Use Prototype and Proto to implement inheritance
Objects are widely used because their attribute values can be of any type. Therefore, the value of the property can also be another object, which means JavaScript can do this by assigning the proto property of object A to object B:
A.__proto__ = B
Copy the code
Use a.proto to access B’s properties and methods.
In this way, JavaScript can create an association between two objects, enabling one object to access the properties and methods of the other, thereby implementing inheritance;
Use Prototype and Proto to implement inheritance
In the case of Person, JavaScript creates an instance of the constructor Person when we use new Person() to create an object. For example, here we create a Person named “Zhangsan” :
var zhangsan = new Person("zhangsan");
Copy the code
At runtime, the JavaScript engine assigns Person’s prototype object prototype to zhangsan’s proto property to implement zhangsan’s inheritance of Person, i.e., execute the following code:
Var zhangsan = {}; zhangsan.__proto__ = Person.prototype; Person.call(zhangsan, "zhangsan");Copy the code
Let’s print a zhangsan instance:
console.log(zhangsan)
Copy the code
The results are shown below:
As can be seen, Zhangsan is the instance object of Person, and its proto points to the prototype object of Person, i.e. Person.prototype.
At this point, we add the relationship in the figure above:
From this picture, we can clearly see the relationships between constructors and constructor properties, prototype objects and proto, and instance objects, which are often confusing. According to this graph, we can get the following relationship:
- Each function’s prototype object (Person.prototype) has a constructor attribute pointing to the prototype object’s constructor (Person);
- You use the constructor (new Person()) to create an object, called an instance object (Lily);
- The instance object implements inheritance from the constructor’s prototype object (Person.prototype) by pointing the proto attribute to that object.
Now, proto’s relationship to Prototype looks something like this:
-
Each object has a proto attribute to identify the prototype object it inherits, but only functions have the Prototype attribute;
-
For functions, each function has a prototype property, which is the prototype object of that function;
-
JavaScript can implement inheritance the way constructors create objects by assigning the proto property of an instance object to its constructor’s prototype object.
So an object can access properties and methods on the prototype object through ProTO, and the prototype can access its prototype object through ProTO, so we construct a prototype chain between the instance and the prototype. The red line shows:
The methods and properties of the object are accessed through the stereotype chain
When JavaScript tries to access an object’s properties, it looks it up based on the prototype chain. The search process looks like this:
- The object is searched first. If it cannot be found, it will successively search the prototype object of the object and the prototype object of the prototype object of the object (nesting alarm);
- All objects in JavaScript come from Object, Object.prototype.proto === null. Null has no prototype and serves as the last link in the prototype chain;
- JavaScript traverses the entire prototype chain of the accessed object, and if it does not find it, it assumes that the object’s property value is undefined.
We can use a concrete example to represent the process of accessing object attributes based on prototype chain. In this example, we build a prototype chain of objects and access attribute values:
var o = {a: 1, b: 2}; __proto__ = {b: 3, c: 4}; // Let's suppose we have an object o with its own attributes a and b: o.__proto__ = {b: 3, c: 4}; // The o prototype o.__proto__ has attributes B and C:Copy the code
When we get the attribute value, we trigger the prototype chain lookup:
console.log(o.a); // o.a => 1
console.log(o.b); // o.b => 2
console.log(o.c); // o.c => o.__proto__.c => 4
console.log(o.d); // o.c => o.__proto__.d => o.__proto__.__proto__ == null => undefined
Copy the code
To sum up, the whole prototype chain is as follows:
{a: 1, b: 2} – > {3, b: c: 4} – > null, / / this is the end of the prototype chain, namely the null
As you can see, when we get the property value of an object, the prototype chain lookup process for that object is triggered.
Since JavaScript accesses an object’s properties by iterating through a prototype chain, we can inherit through a prototype chain.
That is, properties and methods on a prototype object can be accessed through the prototype chain without having to reassign/add methods to the object at creation time. For example, when we call Lily.tostring (), the JavaScript engine does the following:
- First check that the Lily object has a toString() method available;
- If not, check that Lily’s prototype object (Person.prototype) has an available toString() method;
- If not, the toString() method is called by checking that the prototype Object of the Object pointed to by the prototype attribute of the Person() constructor (object.prototype) has an available toString() method.
Due to the search of attributes through the prototype chain, it is necessary to traverse each prototype object layer by layer, which may cause performance problems:
- When an attempt is made to access a nonexistent property, the entire prototype chain is traversed;
- Finding attributes on the prototype chain can be time consuming and has a side effect on performance, which is important when performance is demanding.
Therefore, when designing objects, we need to pay attention to the length of the prototype chain in our code. When the prototype chain is too long, you can choose to decompose it to avoid possible performance problems.
Other ways to implement inheritance
In addition to implementing JavaScript inheritance through prototype chains, there are other ways to implement JavaScript inheritance including classical inheritance (embeded constructors), composite inheritance, primitive inheritance, parasitic inheritance, and so on.
-
In the prototype chain inheritance method, the attribute of reference type is shared by all instances, and the instance cannot be private.
-
Classical inheritance allows instance attributes to be private, but requires the type to be defined only through constructors.
-
Combinatorial inheritance combines the advantages of prototype chain inheritance and constructors. Its implementation is as follows:
Function Parent(name) {// This. Name = name; Parent.prototype.speak = function() {console.log(“hello”); }; function Child(name) { Parent.call(this, name); Prototype = new Parent();
The composite inheritance pattern is the most commonly used inheritance pattern in JavaScript, which enables on-demand sharing of objects and methods by defining shared attributes on the parent class prototype and assigning private attributes through constructors.
Although there are many ways to realize inheritance, in fact, they are inseparable from the content of prototype object and prototype chain. Therefore, mastering the knowledge of Proto and Prototype, object inheritance and so on is the prerequisite for us to realize various inheritance methods.
conclusion
JavaScript archetypes and inheritance often come up in our interview questions. With the advent of new syntactic sugar such as ES6/ES7, there may be a preference for using class/extends to write code, and concepts such as archetypal inheritance fade away.
Second, the design of JavaScript remains essentially the same, still based on archetypal inheritance. If we don’t know these things, it’s easy to do nothing when we come across something that’s out of our reach.