- Prototype inheritance is fineJavaScript is an object-oriented (OO) language, rooted in the
Self language, despite the fact that it looks like Java. This circumstance makes the language really powerful due to some nice features.
One of these features is the implementation of prototype inheritance. This simple concept is flexible and powerful. It allows you to make the inheritance and behavior of first-class entities, as well as functions are first-class objects in functional languages (including JavaScript).
Fortunately,
ECMAScript 5 has a lot of things that allowed you to put the language on the right path (some of them are covered in this article). There will also be talked about the disadvantages of JavaScript design and a small comparison with the classical prototype OO model (including its advantages and disadvantages) will be made.
The article assumes that you are already familiar with the basics of JavaScript, have an idea about functions (including the concepts of closure and first-class functions), primitive values, operators, etc.
')
1. Objects
An object in JavaScript is simply a collection of key-value pairs (and sometimes a bit of internal magic).
However, there is no class concept in JavaScript. For example, an object with
{name: Linda, age: 21} properties is not an instance of any class or class
Object . Both
Object and
Linda are instances of themselves. They are determined directly by their own behavior. There is no meta-data layer (ie classes) that would tell these objects how to behave.
You may ask, “Yes, how so?”, Especially if you come from the world of classical object-oriented languages (such as Java or C #). “But if each object has its own behavior (instead of inheriting it from a general class), then if I have 100 objects, then they correspond to 100 different methods?” Isn't it dangerous? And how do I know that, for example, an object is really an
Array ? ”
To answer all these questions, you need to forget about the classic OO approach and start everything from scratch. Believe me, it's worth it.
The prototype OO model brings several new dynamic and expressive ways to solve old problems. It also presents powerful models for extending and reusing code (and this is what interests people who are talking about object-oriented programming). However, this model gives less guarantees. For example, it cannot be assumed that an object
x will always have the same set of properties.
1.1. What are objects?
It was previously mentioned that objects are simply pairs of unique keys with corresponding values — such pairs are called properties. For example, you want to describe several aspects of your old friend (let's call him Misha, he's
Mikhail ), such as age, name and gender:

An object in JavaScript is created using the
Object.create function. This function from the parent and optional property set creates a new entity. For now, we will not worry about the parameters.
An empty object is an object without a parent, without properties. Let's look at the syntax for creating such an object in JavaScript:
var mikhail = Object.create(null)
1.2. Creating properties
So, then we already have an object, but it does not have any properties yet - we have to correct this situation to describe our object
Mikhail .
JavaScript properties are dynamic. This means that we can create or delete them at any time. Properties are unique in the sense that the property key inside an object matches exactly one value.
Create new properties through the
Object.defineProperty function, which uses the object as arguments, the name of the property to create and a descriptor that describes the semantics of the property.
Object.defineProperty(mikhail, 'name', { value: 'Mikhail' , writable: true , configurable: true , enumerable: true }) Object.defineProperty(mikhail, 'age', { value: 19 , writable: true , configurable: true , enumerable: true }) Object.defineProperty(mikhail, 'gender', { value: 'Male' , writable: true , configurable: true , enumerable: true })
The
Object.defineProperty function creates a new property if a property with a given key did not previously exist (otherwise, the semantics and values of the existing property will be updated).
By the way, you can also use
Object.defineProperties when you need to add more than one property to an object:
Object.defineProperties(mikhail, { name: { value: 'Mikhail' , writable: true , configurable: true , enumerable: true } , age: { value: 19 , writable: true , configurable: true , enumerable: true } , gender: { value: 'Male' , writable: true , configurable: true , enumerable: true }})
Obviously, both calls are similar, they are completely configurable, but not intended for the end user of the code. It is better to create a level of abstraction over them.
1.3. Descriptors
Small objects that contain semantics are called descriptors (we used them when calling
Object.defineProperty ). Descriptors are one of two types - data descriptors and access descriptors.
Both types of descriptors contain flags that define how the property will be considered by the language. If the flag is not set, then its default value is
false (unfortunately this is not always a good default value, which leads to an increase in the amount of descriptor descriptions).
Consider some flags:
- writable - the property value can be changed, used only for data descriptors.
- configurable - the property type can be changed or the property can be deleted.
- enumerable - the property is used in the general enumeration.
Data descriptors are such that they define a specific value that corresponds to an additional value parameter that describes the specific data associated with the property: - value - the value of the property
Access descriptors define access to a specific value through getters and setters of functions. If not set, then defaults to
undefined .
- get () - the function is called without arguments when a request is made to the property value.
- set (new_value) - the function is called with an argument - a new value for the property when the user tries
modify the value of the property.
1.4. Strive for conciseness
Fortunately, property descriptors are not the only way to work with properties in JavaScript — you can create them more succinctly.
JavaScript also understands property references using a so-called bracket entry. The basic rule is written as follows:
<bracket-access> ::= <identifier> "[" <expression> "]"
Here
identifier is a variable that stores an object containing a property whose value we want to set, and
expression is any valid JavaScript expression that defines the name of the property. There are no restrictions on what name a property can have; everything is allowed.
Thus, we can rewrite the previous example:
mikhail['name'] = 'Mikhail' mikhail['age'] = 19 mikhail['gender'] = 'Male'
Note: all property names are ultimately converted to a string, i.e. the
object [1] ,
object [[1]] ,
object ['1'], and
object [variable] records (where the
variable value is 1) are equivalent.
There is another way to refer to a property called a point entry. It looks simpler and more concise than the bracket alternative. However, with this method, the property name must comply with the rules of a
valid JavaScript identifier and cannot be represented by an expression (that is, variables cannot be used).
The general rule for point recording is:
<dot-access> ::= <identifier> "." <identifier-name>
Thus, the previous example has become even more beautiful:
mikhail.name = 'Mikhail' mikhail.age = 19 mikhail.gender = 'Male'
Both syntaxes perform the equivalent process of creating properties, setting semantic flags to
true .
1.5. Access to properties
It is very easy to get the value stored in a given property - the syntax is very similar to creating a property with the only difference being that there is no assignment in it.
For example, if we want to know the age of Misha, then we will write:
mikhail['age']
But if we try to get the value of a property that does not exist in our object, we will get
undefined :
mikhail['address']
1.6. Removing properties
To delete a property from an object, a
delete operator is provided in JavaSCript. For example, if you want to remove the
gender property from our
mikhail object:
delete mikhail['gender']
The
delete operator will return
true if the property has been deleted, and
false otherwise. We will not delve into how this operator works. But if you're still interested, then you can read the
most beautiful article on how delete works .
1.6. Getters and setters
Getters and setters are commonly used in classical object-oriented languages to provide encapsulation. They are not really needed in JavaScript, but, we have a dynamic language, and
I am against this functionality .
But, from any point of view, they allow you to provide proxies for read and write requests for properties. For example, we had separate slots for a name and a surname, but we want to have a convenient way to read and install them.
To begin with, we will create the name and surname of our friend, describing the corresponding properties:
Object.defineProperty(mikhail, 'first_name', { value: 'Mikhail' , writable: true }) Object.defineProperty(mikhail, 'last_name', { value: 'Weiß' , writable: true })
Then we describe the general way of getting and setting two properties at once at once - let's call their union
name :
Now, every time we try to find out the value of our friend’s
name property, the get_full_name function will actually be
called :
mikhail.name
We can also set the
name of the object by accessing the corresponding property, but in fact the call to
set_full_name will do all the dirty work:
mikhail.name = 'Michael White' mikhail.name
There are scenarios in which it is really convenient to do this, but it is worth remembering that such a mechanism works
very slowly .
In addition, it should be noted that getters and setters are usually used in other languages for encapsulation, and in ECMAScript 5 you still cannot do this — all properties of the object are public.
1.8. Enumeration of properties
Due to the fact that the properties are dynamic, JavaScript provides functionality for checking the set of object properties. There are two ways to list all the properties of an object, depending on what kind of properties you are interested in.
The first way is to call the
Object.getOwnPropertyNames function, which will return an
Array containing the names of all the properties set for this object — we will call these properties
our own . For example, let's see what we know about Misha:
Object.getOwnPropertyNames(mikhail)
The second way is to use
Object.keys , which returns a list of its own properties, which are marked with the
enumerable flag:
Object.keys(mikhail)
1.9. Literals
A simple way to create an object is to use the literal syntax of JavaScript. A literal object defines a new object, the parent of which is an
Object.prototype (we'll talk about parents a little later).
In any case, the syntax of literal objects allows you to define simple objects and initialize their properties. Let's rewrite the example of creating the
Mikhail object:
var mikhail = { first_name: 'Mikhail' , last_name: 'Weiß' , age: 19 , gender: 'Male'
Invalid property names can be enclosed in quotes. Note that the entry for getter / setter is defined literally by anonymous functions. If you want to associate a previously declared function with getter / setter, then you must use the
Object.defineProperty method.
Let's look at the general rules of literal syntax:
<object-literal> ::= "{" <property-list> "}" ; <property-list> ::= <property> ["," <property>]* ; <property> ::= <data-property> | <getter-property> | <setter-property> ; <data-property> ::= <property-name> ":" <expression> ; <getter-property> ::= "get" <identifier> : <function-parameters> : <function-block> ; <setter-property> ::= "set" <identifier> : <function-parameters> : <function-block> ; <property-name> ::= <identifier> | <quoted-identifier> ;
Literal objects can appear inside expressions in javascript. Due to some ambiguity, newbies are sometimes confused:
2. Methods
Until now, the
Mikhail object had only data storage slots (well, except for getter / setter for the
name property). The description of actions that can be done with an object is done in JavaScript very simply. Simple - because in JavaScript there is no difference between manipulating things like
Function ,
Number ,
Object . Everything is done in the same way (do not forget that functions in JavaScript are first-class entities).
We describe the action on this object by simply setting the function as the value of our property. For example, we want Misha to greet other people:
After setting the property value, we can use a similar method to set specific data associated with the object. Thus, access to properties will return a reference to the function stored in it, which we can call:
mikhail.greet('you')
2.1. Dynamic this
One thing to consider when describing the
greet function is that this function should refer to the getter / setter of the
name property, and for this it uses the magic variable
this .
It stores a reference to the object to which the executing function belongs. This does not necessarily mean that
this is always equal to the object in which the function
is stored . No, JavaScript is not so selfish.
Functions are
generic . Those. in javascript, the variable
this defines a dynamic reference that is resolved when the function is executed.
This dynamic resolution process provides an incredibly powerful mechanism for dynamizing JavaScript's object-oriented nature and compensates for the lack of strict correspondence to specified structures (that is, classes). This means that you can apply a function to any object that meets the launch requirements, regardless of how the object is structured (as in
CLOS ).
2.2. Permission this
There are four different ways to allow
this in a function, depending on how the function is called: directly, as a method, explicitly applied as a constructor. We will look at the first three, and will return to the designers later.
For the following examples, you will take:
2.2.1 Call as a method
If the function is called as an object method, then
this inside the function refers to the object itself. Those. when we explicitly specify which object performs the action, the object will be the value of
this in our function.
This will happen when we call
mikhail.greet () . This entry tells the javascript that we want to apply the
greet action to the
mikhail object.
one.add(two.value)
2.2.2 Direct Call
When the function is called directly,
this is resolved to the global engine object (
window in browser,
global in Node.js)
add(two.value)
2.2.3. Explicit application
Finally, the function can be explicitly applied to any object, regardless of whether the object has a corresponding property or not. This functionality is achieved using
call or
apply methods.
The difference between the two methods lies in the parameters passed to the function and the execution time —
apply works about 55 times slower than an immediate call, but
call is usually not particularly worse. Everything depends on the current engine, so use
Perf test to be sure - do not optimize the code ahead of time.
In any case,
call expects an object as the first parameter of the function, followed by the usual arguments of the original function:
add.call(two, 2, 2)
On the other hand,
apply allows the second parameter to describe an array of parameters of the original function:
add.apply(two, [2, 2])
On a note. Note that the resolution of
this in
null or
undefined depends on the semantics of the engine used. The result is usually the same as applying a function to a global object. But if the engine runs in
strict mode , then
this will be allowed as expected - exactly to the thing to which it is applied:
window.value = 2 add.call(undefined, 1)
2.3. Method binding
Let us distract from the dynamic essence of functions in JavaScript, go along the path of creating functions, associating them with certain objects, so that
this inside a function always points to this object, whether it is called as an object method or directly.
The function provides the functionality called
bind : the object is taken and an additional parameter (very similar to the call
call ) and a new function is returned, which will apply the parameters to the original function when calling:
var one_add = add.bind(one) one_add(2)
3. Inheritance
So far, we have seen how objects can define their behavior and how we can use their actions on other objects, but, we still have not seen the normal way to reuse code and its extensibility.
This is where inheritance comes in handy. It will allow separating tasks in which objects define specialized behavior from creating common behavior for other objects.
The prototyping model goes further. Although it supports technologies such as “selective extensibility” and “behavior sharing”, we will not particularly study them. The sad thing: the specific prototype OO models implemented in JavaScript are somewhat limited. We can bypass these restrictions, but the overhead will be great.
3.1. Prototypes
Inheritance in JavaScript is done through cloning the behavior of an object and expanding it with specialized behavior. An object whose behavior is cloned is called a
prototype .
A prototype is an ordinary object that shares its behavior with other objects — in this case, it acts as a parent.
The concept of cloning behavior does not mean that you will have two different copies of the same function or data. In fact, JavaScript implements inheritance through delegation, i.e. All properties are stored in the parent, and access to them is extended through the child.
As mentioned earlier, the parent (or
[[Prototype]] ) of an object is determined by calling
Object.create with the first argument referring to the parent object.
Let's go back to the example of Misha. We single out his name and the ability to greet people in a separate object that will share his behavior with Misha. Here is how our model will look like:

We implement it in javascript:
var person = Object.create(null)
3.2 But how does [ [Prototype] ] work?
As you saw in the last example, we did not explicitly define any of the properties defined in
Person explicitly in
Mikhail , but we were still able to access them. This is due to the fact that JavaScript implements the delegation of access to properties, i.e. property is searched through all parents of the object.
This chain of parents is defined by a hidden slot in each object, which is called
[ [Prototype]] . You cannot change it directly, there is only one way to set a value for it - when creating a new object.
When a property is requested from an object, the engine first tries to get the property from the target object. If the property is not found, then the immediate parent of the object is considered, then the parent of the parent, etc.
This means that we can change the behavior of the prototype in the middle of the program, then the behavior of all objects that were inherited from it will automatically change. For example, suppose we want to change the default greeting:
3.3. Property overload
So, prototyping (i.e., inheritance) is used so that you can share data with other objects. Moreover, this method works quickly and is economical with respect to memory, since we always have only one instance of the data used.
, ? , — .
,
Person ,
Person . , :

,
mikhail ,
kristin greet .
greet ,
greet ,
Person :
To be continued...